mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 10:31:47 -03:00
Compare commits
45 Commits
v2.1.0
...
fix/steamd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc664afa52 | ||
|
|
2efecd168f | ||
|
|
225bc662b3 | ||
|
|
8ef13c5ee1 | ||
|
|
778ed11f87 | ||
|
|
24a919588e | ||
|
|
219b50a214 | ||
|
|
4c059f0a6b | ||
|
|
9025800820 | ||
|
|
34ee099ae2 | ||
|
|
e56b12cd72 | ||
|
|
3edee4b4eb | ||
|
|
e5fec7c326 | ||
|
|
7d2672b684 | ||
|
|
01823729ec | ||
|
|
639a2ab1b5 | ||
|
|
6b76eb365e | ||
|
|
6fa933fece | ||
|
|
e7023dcf95 | ||
|
|
faf21b830b | ||
|
|
f4d966ee65 | ||
|
|
ca835a868b | ||
|
|
3a1b6039d0 | ||
|
|
7828454631 | ||
|
|
cc1c6c334c | ||
|
|
081ac926e3 | ||
|
|
75a450c9ec | ||
|
|
e426690632 | ||
|
|
78f76afe0a | ||
|
|
131de1dcd7 | ||
|
|
b39877f561 | ||
|
|
6f10b1390d | ||
|
|
0b1b448cce | ||
|
|
aed00cd067 | ||
|
|
c4a32ce1e0 | ||
|
|
eff6fcd520 | ||
|
|
94d4586b97 | ||
|
|
20faf36b37 | ||
|
|
375b422c73 | ||
|
|
b668bdb45a | ||
|
|
653d4429ed | ||
|
|
17e15c17f0 | ||
|
|
b99b22e8bf | ||
|
|
9303c17e57 | ||
|
|
615ee5cadc |
20
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
20
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -55,6 +55,15 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: hardwarespec
|
||||
attributes:
|
||||
label: Hardware Specification
|
||||
description: Tell us your CPU, iGPU, dGPU, VRAM, and RAM information.
|
||||
placeholder: "CPU: Intel i9-14900K 6.0 GHz | GPU: NVIDIA RTX 4090 | VRAM: 24 GB | RAM: 32 GB"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
@@ -71,8 +80,17 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs or Error Messages
|
||||
description: If applicable, paste any error messages or logs here.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here.
|
||||
description: Add any other context about the problem here.
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -39,7 +39,7 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: screenshots
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots (Optional)
|
||||
@@ -49,4 +49,4 @@ body:
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/support_request.yml
vendored
11
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -28,6 +28,15 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: hardwarespec
|
||||
attributes:
|
||||
label: Hardware Specification
|
||||
description: Tell us your CPU, iGPU, dGPU, VRAM, and RAM information.
|
||||
placeholder: "CPU: Intel i9-14900K 6.0 GHz | GPU: NVIDIA RTX 4090 | VRAM: 24 GB | RAM: 32 GB"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
@@ -66,4 +75,4 @@ body:
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Any other information that might help us assist you.
|
||||
description: Any other information that might help us assist you.
|
||||
|
||||
111
.github/workflows/release.yml
vendored
111
.github/workflows/release.yml
vendored
@@ -9,35 +9,6 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libarchive-tools
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
|
||||
- name: Build Linux Packages
|
||||
run: |
|
||||
npx electron-builder --linux --x64 --arm64 --publish never
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-builds
|
||||
path: |
|
||||
dist/*.AppImage
|
||||
dist/*.AppImage.blockmap
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
dist/*.pkg.tar.zst
|
||||
dist/latest-linux.yml
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
@@ -78,8 +49,82 @@ jobs:
|
||||
dist/*.zip
|
||||
dist/latest-mac.yml
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libarchive-tools
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
|
||||
- name: Build Linux Packages
|
||||
run: |
|
||||
npx electron-builder --linux AppImage deb rpm --x64 --arm64 --publish never
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-builds
|
||||
path: |
|
||||
dist/*.AppImage
|
||||
dist/*.AppImage.blockmap
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
dist/latest-linux.yml
|
||||
|
||||
build-arch:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: archlinux:latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install base packages
|
||||
run: |
|
||||
pacman -Syu --noconfirm
|
||||
pacman -S --noconfirm \
|
||||
base-devel \
|
||||
git \
|
||||
nodejs \
|
||||
npm \
|
||||
rpm-tools \
|
||||
libxcrypt-compat
|
||||
|
||||
- name: Create build user
|
||||
run: |
|
||||
useradd -m builder
|
||||
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
||||
|
||||
- name: Fix Permissions
|
||||
run: chown -R builder:builder .
|
||||
|
||||
- name: Build Arch Package
|
||||
run: |
|
||||
sudo -u builder bash << 'EOF'
|
||||
set -e
|
||||
makepkg --printsrcinfo > .SRCINFO
|
||||
makepkg -s --noconfirm
|
||||
EOF
|
||||
|
||||
- name: Upload Arch Package
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: arch-package
|
||||
path: |
|
||||
*.pkg.tar.zst
|
||||
*.src.tar.zst
|
||||
.SRCINFO
|
||||
|
||||
release:
|
||||
needs: [build-linux, build-windows, build-macos]
|
||||
needs: [build-windows, build-macos, build-linux, build-arch]
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/v') ||
|
||||
@@ -109,11 +154,15 @@ jobs:
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
# If it's a tag, use the tag.
|
||||
tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }}
|
||||
# tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }}
|
||||
# If it's the 'release' branch, use 'v2.0.2-beta.r42'
|
||||
# name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}-beta.r{1}', steps.pkg_version.outputs.VERSION, github.run_number) }}
|
||||
files: |
|
||||
artifacts/arch-package/*.pkg.tar.zst
|
||||
artifacts/arch-package/*.src.tar.zst
|
||||
artifacts/arch-package/.SRCINFO
|
||||
artifacts/linux-builds/**/*
|
||||
artifacts/windows-builds/**/*
|
||||
artifacts/macos-builds/**/*
|
||||
|
||||
@@ -4,9 +4,13 @@ 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)' }
|
||||
{ code: 'tr-TR', name: 'Turkish (Turkey)' },
|
||||
{ code: 'pl-PL', name: 'Polish (Poland)' }
|
||||
];
|
||||
|
||||
// Load single language file
|
||||
|
||||
283
GUI/locales/de.json
Normal file
283
GUI/locales/de.json
Normal file
@@ -0,0 +1,283 @@
|
||||
{
|
||||
"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!"
|
||||
}
|
||||
}
|
||||
235
GUI/locales/fr.json
Normal file
235
GUI/locales/fr.json
Normal file
@@ -0,0 +1,235 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
234
GUI/locales/pl-PL.json
Normal file
234
GUI/locales/pl-PL.json
Normal file
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"nav": {
|
||||
"play": "Graj",
|
||||
"mods": "Mody",
|
||||
"news": "Wiadomości",
|
||||
"chat": "Chat z graczami",
|
||||
"settings": "Ustawienia",
|
||||
"skins": "Skiny"
|
||||
},
|
||||
"header": {
|
||||
"playersLabel": "Graczy:",
|
||||
"manageProfiles": "Zarządzaj Profilami",
|
||||
"defaultProfile": "Domyślny",
|
||||
"f2p": "FREE TO PLAY"
|
||||
},
|
||||
"install": {
|
||||
"title": "FREE TO PLAY LAUNCHER",
|
||||
"playerName": "Nazwa Gracza",
|
||||
"playerNamePlaceholder": "Wprowadź Nazwę",
|
||||
"customInstallation": "Dostosuj Instalacje",
|
||||
"installationFolder": "Folder docelowy",
|
||||
"pathPlaceholder": "Domyślna lokalizacja",
|
||||
"browse": "Przeglądaj",
|
||||
"installButton": "ZAINSTALUJ HYTALE",
|
||||
"installing": "INSTALOWANIE..."
|
||||
},
|
||||
"play": {
|
||||
"ready": "GOTOWE",
|
||||
"subtitle": "Uruchom Hytale i rozpocznij przygodę",
|
||||
"playButton": "GRAJ W HYTALE",
|
||||
"latestNews": "NAJNOWSZE WIADOMOŚCI",
|
||||
"viewAll": "ZOBACZ CAŁOŚĆ",
|
||||
"checking": "SPRAWDZANIE...",
|
||||
"play": "GRAJ"
|
||||
},
|
||||
"mods": {
|
||||
"searchPlaceholder": "Wyszukaj mody...",
|
||||
"myMods": "MOJE MODY",
|
||||
"previous": "POPRZEDNIA",
|
||||
"next": "NASTĘPNA",
|
||||
"page": "Strona",
|
||||
"of": "z",
|
||||
"modalTitle": "MOJE MODY",
|
||||
"noModsFound": "Nie Znaleziono Modów",
|
||||
"noModsFoundDesc": "Spróbuj dostosować wyszukiwanie",
|
||||
"noModsInstalled": "Brak Zainstalowanych Modów",
|
||||
"noModsInstalledDesc": "Dodaj mody z CurseForge lub zaimportuj lokalne pliki",
|
||||
"view": "WIDOK",
|
||||
"install": "ZAINSTALUJ",
|
||||
"installed": "ZAINSTALOWANE",
|
||||
"enable": "WŁĄCZ",
|
||||
"disable": "WYŁĄCZ",
|
||||
"active": "AKTYWNE",
|
||||
"disabled": "WYŁĄCZONE",
|
||||
"delete": "Usuń mod",
|
||||
"noDescription": "Brak opisu",
|
||||
"confirmDelete": "Czy na pewno chcesz usunąć \"{name}\"?",
|
||||
"confirmDeleteDesc": "Tej czynności nie można cofnąć.",
|
||||
"confirmDeletion": "Potwierdź"
|
||||
},
|
||||
"news": {
|
||||
"title": "WSZYSTKIE WIADOMOŚCI",
|
||||
"readMore": "Zobacz Więcej"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Chat z graczami",
|
||||
"pickColor": "Kolor",
|
||||
"inputPlaceholder": "Wprowadź swoją wiadomość...",
|
||||
"send": "Wyślij",
|
||||
"online": "online",
|
||||
"charCounter": "{current}/{max}",
|
||||
"secureChat": "Bezpieczny czat – Linki są ocenzurowane",
|
||||
"joinChat": "Dołącz do Czatu",
|
||||
"chooseUsername": "Wybierz nazwę użytkownika, aby dołączyć do Czatu z graczami",
|
||||
"username": "Nazwa Gracza",
|
||||
"usernamePlaceholder": "Wprowadź swoją nazwę...",
|
||||
"usernameHint": "Między 3-20 znaków, tylko litery, cyfry i znaki - i _",
|
||||
"joinButton": "Dołącz do Czatu",
|
||||
"colorModal": {
|
||||
"title": "Dostosuj Kolor Użytkownika",
|
||||
"chooseSolid": "Wybierz jednolity kolor:",
|
||||
"customColor": "Kolor niestandardowy:",
|
||||
"preview": "Podgląd:",
|
||||
"previewUsername": "Nazwa",
|
||||
"apply": "Zastosuj Kolor"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "USTAWIENIA",
|
||||
"java": "Środowisko Java",
|
||||
"useCustomJava": "Użyj niestandardowej ścieżki Java",
|
||||
"javaDescription": "Zastąp dołączone środowisko wykonawcze Java własnym",
|
||||
"javaPath": "Ścieżka Wykonywalna Java",
|
||||
"javaPathPlaceholder": "Wybierz ścieżkę Java...",
|
||||
"javaBrowse": "Przeglądaj",
|
||||
"javaHint": "Wybierz folder instalacyjny Java (obsługiwane Windows, Mac, Linux)",
|
||||
"discord": "Integracja z Discordem",
|
||||
"enableRPC": "Włącz Discord Rich Presence",
|
||||
"discordDescription": "Pokaż swoją aktywność na Discordzie",
|
||||
"game": "Opcje gry",
|
||||
"playerName": "Nazwa Gracza",
|
||||
"playerNamePlaceholder": "Wprowadź swoją nazwę",
|
||||
"playerNameHint": "Ta nazwa będzie używana w grze (1-16 znaków)",
|
||||
"openGameLocation": "Otwórz Lokalizację Gry",
|
||||
"openGameLocationDesc": "Otwórz folder instalacyjny gry",
|
||||
"account": "Zarządzanie identyfikatorami UUID gracza",
|
||||
"currentUUID": "Obecny UUID",
|
||||
"uuidPlaceholder": "Ładowanie UUID...",
|
||||
"copyUUID": "Skopiuj UUID",
|
||||
"regenerateUUID": "Generuj UUID",
|
||||
"uuidHint": "Twój unikalny identyfikator gracza dla tej nazwy użytkownika",
|
||||
"manageUUIDs": "Zarządzaj wszystkimi UUID",
|
||||
"manageUUIDsDesc": "Wyświetl i zarządzaj wszystkimi identyfikatorami UUID graczy",
|
||||
"language": "Język",
|
||||
"selectLanguage": "Wybierz Język",
|
||||
"repairGame": "Napraw Grę",
|
||||
"reinstallGame": "Zainstaluj ponownie pliki gry (zachowuje dane)",
|
||||
"gpuPreference": "Preferencje GPU",
|
||||
"gpuHint": "Wybierz preferowany procesor graficzny (Linux: wpływa na DRI_PRIME)",
|
||||
"gpuAuto": "Auto",
|
||||
"gpuIntegrated": "Zintegrowana",
|
||||
"gpuDedicated": "Dedykowana",
|
||||
"logs": "SYSTEM LOGS",
|
||||
"logsCopy": "Kopiuj",
|
||||
"logsRefresh": "Odśwież",
|
||||
"logsFolder": "Otwórz Folder",
|
||||
"logsLoading": "Ładowanie logów..."
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "Zarządzanie UUID",
|
||||
"currentUserUUID": "Aktualny UUID użytkownika",
|
||||
"allPlayerUUIDs": "Wszystkie identyfikatory UUID graczy",
|
||||
"generateNew": "Wygeneruj nowy UUID",
|
||||
"loadingUUIDs": "Ładowanie UUID...",
|
||||
"setCustomUUID": "Ustaw niestandardowy UUID",
|
||||
"customPlaceholder": "Wprowadź niestandardowy UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
|
||||
"setUUID": "Ustaw UUID",
|
||||
"warning": "Ostrzeżenie: Ustawienie niestandardowego identyfikatora UUID spowoduje zmianę Twojego obecnego identyfikatora gracza",
|
||||
"copyTooltip": "Kopiuj UUID",
|
||||
"regenerateTooltip": "Wygeneruj nowy UUID"
|
||||
},
|
||||
"profiles": {
|
||||
"modalTitle": "Zarządzaj Profilami",
|
||||
"newProfilePlaceholder": "Nowa Nazwa Profilu",
|
||||
"createProfile": "Utwórz Profil"
|
||||
},
|
||||
"discord": {
|
||||
"notificationText": "Dołącz do naszej społeczności Discord!",
|
||||
"joinButton": "Dołącz Discord"
|
||||
},
|
||||
"skins": {
|
||||
"title": "Skiny",
|
||||
"comingSoon": "Personalizacja skórek już wkrótce..."
|
||||
},
|
||||
"common": {
|
||||
"confirm": "Potwierdź",
|
||||
"cancel": "Anuluj",
|
||||
"save": "Zapisz",
|
||||
"close": "Zamknij",
|
||||
"delete": "Usuń",
|
||||
"edit": "Edytuj",
|
||||
"loading": "Ładowanie...",
|
||||
"apply": "Zastosuj"
|
||||
},
|
||||
"notifications": {
|
||||
"gameDataNotFound": "Błąd: Nie znaleziono danych gry",
|
||||
"gameUpdatedSuccess": "Gra została zaktualizowana pomyślnie! 🎉",
|
||||
"updateFailed": "Aktualizacja nie powiodła się: {error}",
|
||||
"updateError": "Błąd aktualizacji: {error}",
|
||||
"discordEnabled": "Discord Rich Presence włączony",
|
||||
"discordDisabled": "Discord Rich Presence wyłączony",
|
||||
"discordSaveFailed": "Nie udało się zapisać ustawień Discorda",
|
||||
"playerNameRequired": "Proszę podać prawidłową nazwę gracza",
|
||||
"playerNameSaved": "Nazwa gracza została zapisana pomyślnie",
|
||||
"playerNameSaveFailed": "Nie udało się zapisać nazwy gracza",
|
||||
"uuidCopied": "Identyfikator UUID skopiowany do schowka!",
|
||||
"uuidCopyFailed": "Nie udało się skopiować UUID",
|
||||
"uuidRegenNotAvailable": "Ponowna gerowanie UUID niedostępne",
|
||||
"uuidRegenFailed": "Nie udało się ponownie wygenerować UUID",
|
||||
"uuidGenerated": "Nowy UUID został pomyślnie wygenerowany!",
|
||||
"uuidGeneratedShort": "Wygenerowano nowy UUID!",
|
||||
"uuidGenerateFailed": "Nie udało się wygenerować nowego UUID",
|
||||
"uuidRequired": "Wprowadzić UUID",
|
||||
"uuidInvalidFormat": "Nieprawidłowy format UUID",
|
||||
"uuidSetFailed": "Nie udało się ustawić niestandardowego UUID",
|
||||
"uuidSetSuccess": "Niestandardowy UUID został ustawiony pomyślnie!",
|
||||
"uuidDeleteFailed": "Nie udało się usunąć UUID",
|
||||
"uuidDeleteSuccess": "UUID został pomyślnie usunięty!",
|
||||
"modsDownloading": "Pobieranie {name}...",
|
||||
"modsTogglingMod": "Przełączanie moda...",
|
||||
"modsDeletingMod": "Usuwanie moda...",
|
||||
"modsLoadingMods": "Ładowanie modów z CurseForge...",
|
||||
"modsInstalledSuccess": "{name} zainstalowany pomyślnie! 🎉",
|
||||
"modsDeletedSuccess": "{name} usunięto pomyślnie",
|
||||
"modsDownloadFailed": "Nie udało się pobrać moda: {error}",
|
||||
"modsToggleFailed": "Nie udało się przełączyć moda: {error}",
|
||||
"modsDeleteFailed": "Nie udało się usunąć moda: {error}",
|
||||
"modsModNotFound": "Nie znaleziono informacji o modzie"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "Potwierdź działanie",
|
||||
"regenerateUuidTitle": "Wygeneruj nowy UUID",
|
||||
"regenerateUuidMessage": "Czy na pewno chcesz wygenerować nowy UUID? To spowoduje zmianę Twojego identyfikatora gracza.",
|
||||
"regenerateUuidButton": "Generuj",
|
||||
"setCustomUuidTitle": "Ustaw niestandardowy UUID",
|
||||
"setCustomUuidMessage": "Czy na pewno chcesz ustawić ten UUID? To spowoduje zmianę Twojego identyfikatora gracza.",
|
||||
"setCustomUuidButton": "Ustaw UUID",
|
||||
"deleteUuidTitle": "Usuń UUID",
|
||||
"deleteUuidMessage": "Czy na pewno chcesz usunąć UUID dla \"{username}\"? Tej czynności nie można cofnąć.",
|
||||
"deleteUuidButton": "Usuń",
|
||||
"uninstallGameTitle": "Odinstaluj grę",
|
||||
"uninstallGameMessage": "Czy na pewno chcesz odinstalować Hytale? Wszystkie pliki gry zostaną usunięte.",
|
||||
"uninstallGameButton": "Odinstaluj"
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "Inicjalizacja...",
|
||||
"downloading": "Pobieranie...",
|
||||
"installing": "Instalowanie...",
|
||||
"extracting": "Ekstraktowanie...",
|
||||
"verifying": "Weryfikowanie...",
|
||||
"switchingProfile": "Przełączanie profilu...",
|
||||
"profileSwitched": "Profil zmieniony!",
|
||||
"startingGame": "Uruchamianie gry...",
|
||||
"launching": "URUCHAMIANIE...",
|
||||
"uninstallingGame": "Odinstalowywanie gry...",
|
||||
"gameUninstalled": "Gra została pomyślnie odinstalowana!",
|
||||
"uninstallFailed": "Odinstalowanie nie powiodło się: {error}",
|
||||
"startingUpdate": "Rozpoczynanie obowiązkowej aktualizacji gry...",
|
||||
"installationComplete": "Instalacja zakończona pomyślnie!",
|
||||
"installationFailed": "Instalacja nie powiodła się: {error}",
|
||||
"installingGameFiles": "Instalowanie plików gry...",
|
||||
"installComplete": "Instalacja zakończona!"
|
||||
}
|
||||
}
|
||||
283
GUI/locales/sv.json
Normal file
283
GUI/locales/sv.json
Normal file
@@ -0,0 +1,283 @@
|
||||
{
|
||||
"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!"
|
||||
}
|
||||
}
|
||||
33
PKGBUILD
33
PKGBUILD
@@ -1,33 +1,28 @@
|
||||
# Maintainer: Terromur <terromuroz@proton.me>
|
||||
pkgname=Hytale-F2P-git
|
||||
_pkgname=Hytale-F2P
|
||||
pkgver=2.0.12.r150.gb62ffc1
|
||||
# Maintainer: Fazri Gading <fazrigading@gmail.com>
|
||||
# This PKGBUILD is for Github Releases
|
||||
pkgname=Hytale-F2P
|
||||
pkgver=2.1.1
|
||||
pkgrel=1
|
||||
pkgdesc="Hytale-F2P - unofficial Hytale Launcher for free to play with multiplayer support"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/amiayweb/Hytale-F2P"
|
||||
license=('custom')
|
||||
makedepends=('npm' 'git' 'rpm-tools' 'libxcrypt-compat')
|
||||
source=("git+$url.git" "Hytale-F2P.desktop")
|
||||
depends=('gtk3' 'nss' 'libxcrypt-compat')
|
||||
makedepends=('npm')
|
||||
source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop")
|
||||
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
||||
|
||||
pkgver() {
|
||||
cd "$_pkgname"
|
||||
version=$(git describe --abbrev=0 --tags --match "v[0-9]*")
|
||||
commits=$(git rev-list --count HEAD)
|
||||
hash=$(git rev-parse --short HEAD)
|
||||
printf "%s.r%s.g%s" "${version#v}" "$commits" "$hash"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$_pkgname"
|
||||
cd "$pkgname-$pkgver"
|
||||
npm ci
|
||||
npm run build:linux
|
||||
npm run build:arch
|
||||
}
|
||||
|
||||
package() {
|
||||
mkdir -p "$pkgdir/opt/$_pkgname"
|
||||
cp -r "$_pkgname/dist/linux-unpacked/"* "$pkgdir/opt/$_pkgname"
|
||||
install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
|
||||
install -Dm644 "$_pkgname/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"
|
||||
}
|
||||
|
||||
34
PKGBUILD-git
Normal file
34
PKGBUILD-git
Normal file
@@ -0,0 +1,34 @@
|
||||
# Maintainer: Terromur <terromuroz@proton.me>
|
||||
# Maintainer: Fazri Gading <fazrigading@gmail.com>
|
||||
pkgname=Hytale-F2P-git
|
||||
_pkgname=Hytale-F2P
|
||||
pkgver=0
|
||||
pkgrel=1
|
||||
pkgdesc="Hytale-F2P - Unofficial Hytale Launcher for free to play with multiplayer support (rolling git build)"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/amiayweb/Hytale-F2P"
|
||||
license=('custom')
|
||||
depends=('gtk3' 'nss' 'libxcrypt-compat')
|
||||
makedepends=('git' 'npm')
|
||||
source=("git+$url.git" "$_pkgname.desktop")
|
||||
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
||||
|
||||
pkgver() {
|
||||
cd "$srcdir/$_pkgname"
|
||||
git describe --tags --long | sed 's/^v//;s/-/.r/;s/-/./'
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$srcdir/$_pkgname"
|
||||
npm ci
|
||||
npm run build:arch
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$srcdir/$_pkgname"
|
||||
install -d "$pkgdir/opt/$_pkgname"
|
||||
cp -r "$_pkgname/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"
|
||||
}
|
||||
85
README.md
85
README.md
@@ -1,12 +1,13 @@
|
||||
<div align="center">
|
||||
|
||||
<header>
|
||||
<h1>🎮 Hytale F2P Launcher | Cross-Platform Multiplayer 🖥️</h1>
|
||||
<h1>🎮 Hytale F2P Launcher 🚀</h1>
|
||||
<h2>💻 Cross-Platform Multiplayer 🖥️</h2>
|
||||
<h3>Available for Windows 🪟, macOS 🍎, and Linux 🐧</h3>
|
||||
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)</small></p>
|
||||
</header>
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -17,10 +18,10 @@
|
||||
|
||||
### ⚠️ **READ [QUICK START](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-quick-start) before Downloading & Installing the Launcher!** ⚠️
|
||||
|
||||
🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑
|
||||
#### 🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑
|
||||
|
||||
<p>
|
||||
If you like the project, <b>feel free to support us via Buy Me a Coffee!</b>
|
||||
👍 If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> ☕<br>
|
||||
Any support is appreciated and helps keep the project going.
|
||||
</p>
|
||||
|
||||
@@ -119,7 +120,7 @@
|
||||
<td colspan="3" align="center">
|
||||
Windows 10/11 (64-bit; X64/ARM64) | Linux (x64/ARM64) | macOS (Apple Silicon only)
|
||||
<br />
|
||||
<small><i>⚠️ Note: macOS Intel (x86) is not yet supported <a href="#fn1" id="ref1">1</a></sup></i></small>
|
||||
<small><i>⚠️ Note: macOS Intel (x86) is not yet supported <sup><a href="#fn1" id="ref1">1</a></sup></i></small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -130,7 +131,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>🧠 RAM</b></td>
|
||||
<td>8GB (Dedicated) / 12GB (iGPU)</td>
|
||||
<td>8GB (dGPU)<sup><a href="#fn1" id="ref2">2</a></sup> /<br>12GB (iGPU)<sup><a href="#fn1" id="ref3">3</a></sup></td>
|
||||
<td>16 GB</td>
|
||||
<td>32 GB</td>
|
||||
</tr>
|
||||
@@ -155,14 +156,22 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p id="fn1"><sup>1</sup> Hytale did not provide game files for macOS Intel, yet.</p>
|
||||
<p id="fn1"><sup>Note 1</sup> Hytale did not provide game files for macOS Intel, yet.</p>
|
||||
<p id="fn2"><sup>Note 2</sup> Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum.</p>
|
||||
<p id="fn3"><sup>Note 3</sup> Using Integrated GPU (dGPU) must have 12 GB RAM minimum.</p>
|
||||
|
||||
|
||||
|
||||
### 🪟 Windows Prequisites
|
||||
* **Java JDK 25:** Download via [Adoptium](https://adoptium.net/temurin/releases/?version=25) or [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows)
|
||||
* **Latest Visual Studio Redist:** Download via [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe) or [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/)
|
||||
* **ENABLE MULTIPLAYER:** // TODO MULTIPLAYER GUIDE; FIREWALL GUIDE AND SUCH
|
||||
* **
|
||||
* **Java JDK 25:**
|
||||
* [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows), **no** support for Windows ARM64 in both version 25 and 21.
|
||||
* [Adoptium](https://adoptium.net/temurin/releases/?version=25), has Windows ARM64 support in version 21 only.
|
||||
* [Microsoft](https://learn.microsoft.com/en-us/java/openjdk/download), has Windows ARM64 support in version 25.
|
||||
* Download from any vendor if your OS is not Windows with ARM64 architecture.
|
||||
* **Latest Visual Studio Redist:**
|
||||
* Download via [Microsoft Visual C++ Redistributable](https://aka.ms/vc14/vc_redist.x64.exe)
|
||||
* Or [All-in-One by Techpowerup](https://www.techpowerup.com/download/visual-c-redistributable-runtime-package-all-in-one/)
|
||||
|
||||
### 🐧 Linux Prequisites
|
||||
|
||||
@@ -202,24 +211,37 @@
|
||||
3. **Permissions & Execution:**
|
||||
* **AppImage:** Make the file executable and run it:
|
||||
```bash
|
||||
chmod +x Hytale-F2P-Launcher.AppImage
|
||||
./Hytale-F2P-Launcher.AppImage
|
||||
chmod +x hytale-f2p-launcher.AppImage
|
||||
./hytale-f2p-launcher.AppImage
|
||||
```
|
||||
* **Fedora (dnf):** Install the RPM:
|
||||
* **Ubuntu/Debian-based or Fedora/RHEL-based:** Install the DEB/RPM:
|
||||
```bash
|
||||
sudo dnf install ./Hytale-F2P-Launcher.rpm
|
||||
```
|
||||
* **Debian/Ubuntu (apt):** Install the DEB:
|
||||
```bash
|
||||
sudo apt install ./Hytale-F2P-Launcher.deb
|
||||
# Fedora/RHEL-based
|
||||
sudo dnf install hytale-f2p-launcher.rpm
|
||||
# Debian/Ubuntu
|
||||
sudo apt install -y libasound2 libpng16-16 libpng-dev libicu76
|
||||
sudo dpkg -i hytale-f2p-launcher.deb
|
||||
```
|
||||
* **Arch Linux (pacman):** Install the package using:
|
||||
```bash
|
||||
sudo pacman -U /path/to/Hytale-F2P-Launcher.pkg.tar.zst
|
||||
# Stable Build
|
||||
sudo pacman -U hytale-f2p-launcher.pkg.tar.zst
|
||||
# Development Build
|
||||
yay -S hytale-f2p-git # or
|
||||
paru -S hytale-f2p-git
|
||||
# Manual Build
|
||||
git clone https://aur.archlinux.org/hytale-f2p-git.git
|
||||
cd hytale-f2p-git
|
||||
makepkg -si
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Make sure to adjust the filename correctly with the version and the architecture type. TIP: Use `cd` command to the package location.
|
||||
|
||||
4. **Troubleshooting:**
|
||||
* **FUSE:** If the AppImage fails to launch on newer distributions, ensure `libfuse2` (or `fuse2` on Arch/Fedora) is installed.
|
||||
* **Desktop Entry:** After installing via `.rpm`, `.deb`, or `.pkg.tar.zst`, the launcher should automatically appear in your App Library/Grid.
|
||||
* Missing libxcrypt.so.1: Install `libxcrypt-compat` using your package manager
|
||||
|
||||
---
|
||||
|
||||
@@ -279,27 +301,28 @@ The `.zip` version is useful for users who prefer a portable installation or nee
|
||||
|
||||
## 🛠️ Building from Source
|
||||
|
||||
See [BUILD.md](BUILD.md) for comprehensive build instructions.
|
||||
See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Changelog
|
||||
|
||||
### 🆕 v2.1.0
|
||||
|
||||
- 🚨 **Auto-Retry Downloads and Auto-Patch Files** —
|
||||
- ⚡ **Hardware Acceleration** —
|
||||
- 👨💻 **In-App Logging** —
|
||||
- 🛠️ **Repair Button** — Y
|
||||
- 🔎 **Browse CurseForge Mods** — Browsing mods now easier with our dedicated CurseForge API Key.
|
||||
- 🌎 **Fixes and Release New Translation** — Fixed 🇪🇸 🇧🇷 and added more translation for current build. Turkish 🇹🇷 language now added.
|
||||
|
||||
|
||||
### 🆕 v2.1.1
|
||||
- 🛠️ **Fix Bug EPERM**: EPERM or Error Permission in creating/removing process in reinstalling is now fixed.
|
||||
- 🅰️ **Adds .pkg.tar.zst Build for Arch Users**: This Arch-package has been needed since the first release.
|
||||
- ❎ **Removes .pacman Build for Arch**: Based on the established conventions within the Arch Linux community, the file extension .pacman should not be used for package files.
|
||||
- 🌎 **New Translation**: New Polish 🇵🇱 Translation added to the Launcher.
|
||||
|
||||
<details>
|
||||
<summary>Click here to see older Changelogs</summary>
|
||||
|
||||
### 🆕 v2.0.2b *(Minor Update: Performance & Utilities)*
|
||||
### 🔄 v2.1.0
|
||||
- 🚨 **Auto-Retry Downloads and Auto-Patch Files** —
|
||||
- ⚡ **Hardware Acceleration** —
|
||||
- 🔎 **Browse CurseForge Mods** — Browsing mods now easier with our dedicated CurseForge API Key.
|
||||
- 🌎 **Fixes and Release New Translation** — Fixed 🇪🇸 🇧🇷 and added more translation for current build. Turkish 🇹🇷 language now added.
|
||||
|
||||
### 🔄 v2.0.2b *(Minor Update: Performance & Utilities)*
|
||||
- 🌎 **Language Translation** — A big welcome for Spanish 🇪🇸 and Portuguese (Brazil) 🇧🇷 players! **Language setting can be found in the bottom part of Settings pane.**
|
||||
- 💻 **Laptop/Hybrid GPU Performance Issue Fix** — Added automatic GPU detection system and options to choose which GPU will be used for the game, *specifically for Linux users*.
|
||||
- 👨💻 **In-App Logging** — Reporting bugs and issues to `Github Issues` tab or `Open A Ticket` channel in our Discord Server has been made easier for players, no more finding logs file manually.
|
||||
|
||||
@@ -14,6 +14,21 @@ 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) {
|
||||
@@ -179,8 +194,28 @@ async function getModsPath(customInstallPath = null) {
|
||||
const profilesPath = path.join(userDataPath, 'Profiles');
|
||||
|
||||
if (!fs.existsSync(modsPath)) {
|
||||
// Ensure the Mods directory exists
|
||||
fs.mkdirSync(modsPath, { recursive: true });
|
||||
// Check for broken symlink to avoid EEXIST/EPERM on mkdir
|
||||
let isBrokenLink = false;
|
||||
let pathExists = false;
|
||||
try {
|
||||
const stats = fs.lstatSync(modsPath);
|
||||
pathExists = true;
|
||||
if (stats.isSymbolicLink()) {
|
||||
// Check if target exists
|
||||
try {
|
||||
fs.statSync(modsPath);
|
||||
} catch {
|
||||
isBrokenLink = true;
|
||||
}
|
||||
}
|
||||
} catch (e) { /* path doesn't exist at all */ }
|
||||
|
||||
if (isBrokenLink) {
|
||||
fs.unlinkSync(modsPath); // Remove broken symlink
|
||||
}
|
||||
if (!pathExists || isBrokenLink) {
|
||||
fs.mkdirSync(modsPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
if (!fs.existsSync(disabledModsPath)) {
|
||||
fs.mkdirSync(disabledModsPath, { recursive: true });
|
||||
@@ -198,20 +233,8 @@ async function getModsPath(customInstallPath = null) {
|
||||
|
||||
function getProfilesDir(customInstallPath = null) {
|
||||
try {
|
||||
// get UserData path
|
||||
let installPath = customInstallPath;
|
||||
if (!installPath) {
|
||||
const configFile = path.join(DEFAULT_APP_DIR, 'config.json');
|
||||
if (fs.existsSync(configFile)) {
|
||||
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
||||
installPath = config.installPath || '';
|
||||
}
|
||||
}
|
||||
if (!installPath) installPath = getAppDir();
|
||||
|
||||
const branch = loadVersionBranch();
|
||||
const gameLatest = path.join(installPath, branch, 'package', 'game', 'latest');
|
||||
const userDataPath = findUserDataPath(gameLatest);
|
||||
// NEW 2.1.2: Use centralized UserData location
|
||||
const userDataPath = getHytaleSavesDir();
|
||||
const profilesDir = path.join(userDataPath, 'Profiles');
|
||||
|
||||
if (!fs.existsSync(profilesDir)) {
|
||||
@@ -227,6 +250,7 @@ function getProfilesDir(customInstallPath = null) {
|
||||
|
||||
module.exports = {
|
||||
getAppDir,
|
||||
getHytaleSavesDir,
|
||||
getResolvedAppDir,
|
||||
expandHome,
|
||||
APP_DIR,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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;
|
||||
@@ -23,6 +24,57 @@ 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();
|
||||
@@ -106,7 +158,9 @@ 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');
|
||||
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
|
||||
|
||||
// NEW 2.1.2: Use centralized UserData location
|
||||
const userDataDir = getUserDataPath();
|
||||
|
||||
const gameLatest = customGameDir;
|
||||
let clientPath = findClientPath(gameLatest);
|
||||
@@ -282,6 +336,64 @@ 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'],
|
||||
@@ -294,7 +406,19 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
spawnOptions.windowsHide = true;
|
||||
}
|
||||
|
||||
const child = spawn(clientPath, args, spawnOptions);
|
||||
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);
|
||||
}
|
||||
|
||||
console.log(`Game process started with PID: ${child.pid}`);
|
||||
|
||||
|
||||
@@ -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 userDataBackup = require('../utils/userDataBackup');
|
||||
const { getUserDataPath, migrateUserDataToCentralized } = require('../utils/userDataMigration');
|
||||
|
||||
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
||||
const osName = getOS();
|
||||
@@ -308,31 +308,25 @@ 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 hasVersionConfig = !!(config.version_client && config.version_branch);
|
||||
const oldBranch = config.version_branch || 'release'; // L'ancienne branche pour le backup
|
||||
console.log(`[UpdateGameFiles] hasVersionConfig: ${hasVersionConfig}`);
|
||||
const oldBranch = config.version_branch || 'release';
|
||||
console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`);
|
||||
|
||||
try {
|
||||
if (progressCallback) {
|
||||
progressCallback('Backing up user data...', 5, null, null, null);
|
||||
}
|
||||
|
||||
// Backup UserData AVANT de télécharger/installer (critical for same-branch updates)
|
||||
// NEW 2.1.2: Ensure UserData migration to centralized location
|
||||
try {
|
||||
console.log(`[UpdateGameFiles] Attempting to backup UserData from old branch: ${oldBranch}`);
|
||||
backupPath = await userDataBackup.backupUserData(installPath, oldBranch, hasVersionConfig);
|
||||
if (backupPath) {
|
||||
console.log(`[UpdateGameFiles] ✓ UserData backed up from ${oldBranch}: ${backupPath}`);
|
||||
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');
|
||||
}
|
||||
} catch (backupError) {
|
||||
console.warn('[UpdateGameFiles] ✗ UserData backup failed:', backupError.message);
|
||||
} catch (migrationError) {
|
||||
console.warn('[UpdateGameFiles] UserData migration warning:', migrationError.message);
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
@@ -365,7 +359,21 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
||||
|
||||
if (fs.existsSync(gameDir)) {
|
||||
console.log('Removing old game files...');
|
||||
fs.rmSync(gameDir, { recursive: true, force: true });
|
||||
let retries = 3;
|
||||
while (retries > 0) {
|
||||
try {
|
||||
fs.rmSync(gameDir, { recursive: true, force: true });
|
||||
break;
|
||||
} catch (err) {
|
||||
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
|
||||
retries--;
|
||||
console.log(`[UpdateGameFiles] Removal failed with ${err.code}, retrying in 1s... (${retries} retries left)`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.renameSync(tempUpdateDir, gameDir);
|
||||
@@ -376,31 +384,9 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
||||
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
|
||||
console.log('Logo@2x.png update result after update:', logoResult);
|
||||
|
||||
// Ensure UserData directory exists
|
||||
const userDataDir = path.join(gameDir, 'Client', 'UserData');
|
||||
if (!fs.existsSync(userDataDir)) {
|
||||
console.log(`[UpdateGameFiles] Creating UserData directory at: ${userDataDir}`);
|
||||
fs.mkdirSync(userDataDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Restoring user data...', 90, null, null, null);
|
||||
}
|
||||
|
||||
// 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');
|
||||
}
|
||||
// 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');
|
||||
|
||||
console.log(`Game files updated successfully to version: ${newVersion}`);
|
||||
|
||||
@@ -420,15 +406,6 @@ 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 });
|
||||
}
|
||||
@@ -456,28 +433,18 @@ 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] Attempting UserData backup (hasVersionConfig: ${hasVersionConfig})...`);
|
||||
backupPath = await userDataBackup.backupUserData(customAppDir, branch, hasVersionConfig);
|
||||
if (backupPath) {
|
||||
console.log(`[InstallGame] ✓ UserData backed up to: ${backupPath}`);
|
||||
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');
|
||||
}
|
||||
} catch (backupError) {
|
||||
console.warn('[InstallGame] ✗ UserData backup failed:', backupError.message);
|
||||
} catch (migrationError) {
|
||||
console.warn('[InstallGame] UserData migration warning:', migrationError.message);
|
||||
}
|
||||
|
||||
[customAppDir, customCacheDir, customToolsDir].forEach(dir => {
|
||||
@@ -486,10 +453,6 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
||||
}
|
||||
});
|
||||
|
||||
if (!fs.existsSync(userDataDir)) {
|
||||
fs.mkdirSync(userDataDir, { recursive: true });
|
||||
}
|
||||
|
||||
saveUsername(playerName);
|
||||
if (installPathOverride) {
|
||||
saveInstallPath(installPathOverride);
|
||||
@@ -581,29 +544,9 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
||||
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
|
||||
console.log('Logo@2x.png update result after installation:', logoResult);
|
||||
|
||||
// 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');
|
||||
}
|
||||
// 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');
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Installation complete', 100, null, null, null);
|
||||
|
||||
@@ -2,6 +2,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
const { getOS } = require('../utils/platformUtils');
|
||||
const { getModsPath, getProfilesDir } = require('../core/paths');
|
||||
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
|
||||
const profileManager = require('./profileManager');
|
||||
@@ -307,11 +308,16 @@ async function syncModsForCurrentProfile() {
|
||||
|
||||
// 2. Symlink / Migration Logic
|
||||
let needsLink = false;
|
||||
let globalStats = null;
|
||||
|
||||
try {
|
||||
globalStats = fs.lstatSync(globalModsPath);
|
||||
} catch (e) {
|
||||
// Path doesn't exist
|
||||
}
|
||||
|
||||
if (fs.existsSync(globalModsPath)) {
|
||||
const stats = fs.lstatSync(globalModsPath);
|
||||
|
||||
if (stats.isSymbolicLink()) {
|
||||
if (globalStats) {
|
||||
if (globalStats.isSymbolicLink()) {
|
||||
const linkTarget = fs.readlinkSync(globalModsPath);
|
||||
// Normalize paths for comparison
|
||||
if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) {
|
||||
@@ -319,7 +325,7 @@ async function syncModsForCurrentProfile() {
|
||||
fs.unlinkSync(globalModsPath);
|
||||
needsLink = true;
|
||||
}
|
||||
} else if (stats.isDirectory()) {
|
||||
} else if (globalStats.isDirectory()) {
|
||||
// MIGRATION: It's a real directory. Move contents to profile.
|
||||
console.log('[ModManager] Migrating global mods folder to profile folder...');
|
||||
const files = fs.readdirSync(globalModsPath);
|
||||
@@ -349,7 +355,20 @@ async function syncModsForCurrentProfile() {
|
||||
|
||||
// Remove the directory so we can link it
|
||||
try {
|
||||
fs.rmSync(globalModsPath, { recursive: true, force: true });
|
||||
let retries = 3;
|
||||
while (retries > 0) {
|
||||
try {
|
||||
fs.rmSync(globalModsPath, { recursive: true, force: true });
|
||||
break;
|
||||
} catch (err) {
|
||||
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
|
||||
retries--;
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
needsLink = true;
|
||||
} catch (e) {
|
||||
console.error('Failed to remove global mods dir:', e);
|
||||
@@ -364,8 +383,8 @@ async function syncModsForCurrentProfile() {
|
||||
if (needsLink) {
|
||||
console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`);
|
||||
try {
|
||||
// 'junction' is key for Windows without admin
|
||||
fs.symlinkSync(profileModsPath, globalModsPath, 'junction');
|
||||
const symlinkType = getOS() === 'windows' ? 'junction' : 'dir';
|
||||
fs.symlinkSync(profileModsPath, globalModsPath, symlinkType);
|
||||
} catch (err) {
|
||||
// If we can't create the symlink, try creating the directory first
|
||||
console.error('[ModManager] Failed to create symlink. Falling back to direct folder mode.');
|
||||
|
||||
@@ -65,15 +65,13 @@ class ClientPatcher {
|
||||
*/
|
||||
getDomainStrategy(domain) {
|
||||
if (domain.length <= 10) {
|
||||
// Direct replacement - subdomains will be stripped
|
||||
return {
|
||||
mode: 'direct',
|
||||
mainDomain: domain,
|
||||
subdomainPrefix: '', // Empty = subdomains stripped
|
||||
subdomainPrefix: '',
|
||||
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 {
|
||||
@@ -88,20 +86,16 @@ 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)); // length byte + padding + chars + separators
|
||||
const result = Buffer.alloc(4 + length + (length - 1));
|
||||
|
||||
// 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);
|
||||
@@ -148,7 +142,7 @@ class ClientPatcher {
|
||||
|
||||
/**
|
||||
* Replace bytes in buffer - only overwrites the length of new bytes
|
||||
* Prevents offset corruption by not expanding the replacement
|
||||
* Does NOT null-pad to avoid corrupting adjacent data
|
||||
*/
|
||||
replaceBytes(buffer, oldBytes, newBytes) {
|
||||
let count = 0;
|
||||
@@ -162,7 +156,7 @@ class ClientPatcher {
|
||||
const positions = this.findAllOccurrences(result, oldBytes);
|
||||
|
||||
for (const pos of positions) {
|
||||
// Only overwrite the length of the new bytes
|
||||
// Only overwrite the length of the new bytes - don't null-fill!
|
||||
newBytes.copy(result, pos);
|
||||
count++;
|
||||
}
|
||||
@@ -171,8 +165,7 @@ class ClientPatcher {
|
||||
}
|
||||
|
||||
/**
|
||||
* UTF-8 domain replacement for Java JAR files.
|
||||
* Java stores strings in UTF-8 format in the constant pool.
|
||||
* UTF-8 domain replacement for Java JAR files
|
||||
*/
|
||||
findAndReplaceDomainUtf8(data, oldDomain, newDomain) {
|
||||
let count = 0;
|
||||
@@ -186,22 +179,23 @@ 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 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
|
||||
* Smart domain replacement for .NET AOT binaries
|
||||
*/
|
||||
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));
|
||||
|
||||
@@ -217,18 +211,9 @@ 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++;
|
||||
}
|
||||
}
|
||||
@@ -238,7 +223,6 @@ 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);
|
||||
@@ -247,100 +231,34 @@ class ClientPatcher {
|
||||
|
||||
console.log(` Patching strategy: ${strategy.description}`);
|
||||
|
||||
// 1. Patch telemetry/sentry URL
|
||||
const oldSentry = 'https://ca900df42fcf57d4dd8401a86ddd7da2@sentry.hytale.com/2';
|
||||
const newSentry = `${protocol}t@${domain}/2`;
|
||||
// ULTRA-MINIMAL PATCHING - only domain, no subdomain patches
|
||||
console.log(` Ultra-minimal mode: only patching main domain`);
|
||||
|
||||
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}`);
|
||||
// Only patch main domain (hytale.com -> 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(` Replaced ${domainResult.count} domain occurrence(s)`);
|
||||
console.log(` Patched ${domainResult.count} domain occurrence(s)`);
|
||||
totalCount += domainResult.count;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
// Skip ALL subdomain patches - let them stay as sessions.hytale.com etc
|
||||
console.log(` Skipping all subdomain patches (ultra-minimal mode)`);
|
||||
|
||||
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'));
|
||||
@@ -358,7 +276,7 @@ class ClientPatcher {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Flag file corrupt or unreadable
|
||||
// Flag file corrupt
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -378,8 +296,7 @@ class ClientPatcher {
|
||||
patchMode: strategy.mode,
|
||||
mainDomain: strategy.mainDomain,
|
||||
subdomainPrefix: strategy.subdomainPrefix,
|
||||
patcherVersion: '2.0.0',
|
||||
verified: 'binary_contents'
|
||||
patcherVersion: '2.1.0'
|
||||
};
|
||||
fs.writeFileSync(patchFlagFile, JSON.stringify(flagData, null, 2));
|
||||
}
|
||||
@@ -395,12 +312,10 @@ 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)}`);
|
||||
@@ -433,22 +348,15 @@ class ClientPatcher {
|
||||
|
||||
/**
|
||||
* Patch the client binary to use the custom domain
|
||||
* @param {string} clientPath - Path to the HytaleClient binary
|
||||
* @param {function} progressCallback - Optional callback for progress updates
|
||||
* @returns {object} Result object with success status and details
|
||||
*/
|
||||
async patchClient(clientPath, progressCallback) {
|
||||
const newDomain = this.getNewDomain();
|
||||
const strategy = this.getDomainStrategy(newDomain);
|
||||
|
||||
console.log('=== Client Patcher v2.0 ===');
|
||||
console.log('=== Client Patcher v2.1 ===');
|
||||
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}`;
|
||||
@@ -458,41 +366,29 @@ 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 (length-prefixed format)...');
|
||||
console.log('Applying domain patches...');
|
||||
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
||||
|
||||
console.log('Patching Discord URLs...');
|
||||
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
|
||||
|
||||
if (count === 0 && discordCount === 0) {
|
||||
if (count === 0) {
|
||||
// Try legacy UTF-16LE format
|
||||
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`);
|
||||
@@ -501,40 +397,31 @@ class ClientPatcher {
|
||||
return { success: true, patchCount: legacyResult.count, format: 'legacy' };
|
||||
}
|
||||
|
||||
console.log('No occurrences found - binary may already be modified or has different format');
|
||||
console.log('No occurrences found - binary may already be modified');
|
||||
return { success: true, patchCount: 0, warning: 'No occurrences found' };
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Writing patched binary...', 80);
|
||||
}
|
||||
if (progressCallback) progressCallback('Writing patched binary...', 80);
|
||||
|
||||
console.log('Writing patched binary...');
|
||||
fs.writeFileSync(clientPath, finalData);
|
||||
|
||||
fs.writeFileSync(clientPath, patchedData);
|
||||
this.markAsPatched(clientPath);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Patching complete', 100);
|
||||
}
|
||||
if (progressCallback) progressCallback('Patching complete', 100);
|
||||
|
||||
console.log(`Successfully patched ${count} domain occurrences and ${discordCount} Discord URLs`);
|
||||
console.log(`Successfully patched ${count} occurrences`);
|
||||
console.log('=== Patching Complete ===');
|
||||
|
||||
return { success: true, patchCount: count + discordCount };
|
||||
return { success: true, patchCount: count };
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 TEMP SYSTEM NEED TO BE FIXED ===');
|
||||
console.log('=== Server Patcher ===');
|
||||
console.log(`Target: ${serverPath}`);
|
||||
console.log(`Domain: ${newDomain}`);
|
||||
|
||||
@@ -544,7 +431,6 @@ class ClientPatcher {
|
||||
return { success: false, error };
|
||||
}
|
||||
|
||||
// Check if already patched
|
||||
const patchFlagFile = serverPath + '.dualauth_patched';
|
||||
if (fs.existsSync(patchFlagFile)) {
|
||||
try {
|
||||
@@ -555,16 +441,14 @@ class ClientPatcher {
|
||||
return { success: true, alreadyPatched: true };
|
||||
}
|
||||
} catch (e) {
|
||||
// Flag file corrupt, re-patch
|
||||
// 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');
|
||||
|
||||
@@ -573,55 +457,37 @@ class ClientPatcher {
|
||||
const url = 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
https.get(url, (response) => {
|
||||
const handleResponse = (response) => {
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
// 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}`));
|
||||
https.get(response.headers.location, handleResponse).on('error', reject);
|
||||
return;
|
||||
}
|
||||
}).on('error', (err) => {
|
||||
|
||||
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) => {
|
||||
fs.unlink(serverPath, () => {});
|
||||
reject(err);
|
||||
});
|
||||
@@ -629,12 +495,11 @@ class ClientPatcher {
|
||||
|
||||
console.log(' Download successful');
|
||||
|
||||
// Mark as patched
|
||||
fs.writeFileSync(patchFlagFile, JSON.stringify({
|
||||
domain: newDomain,
|
||||
patchedAt: new Date().toISOString(),
|
||||
patcher: 'PrePatchedDownload',
|
||||
source: 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar'
|
||||
source: url
|
||||
}));
|
||||
|
||||
if (progressCallback) progressCallback('Server patching complete', 100);
|
||||
@@ -643,8 +508,7 @@ 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);
|
||||
@@ -656,288 +520,38 @@ class ClientPatcher {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Java executable - uses bundled JRE first (same as game uses)
|
||||
* Falls back to system Java if bundled not available
|
||||
* Find Java executable
|
||||
*/
|
||||
findJava() {
|
||||
// 1. Try bundled JRE first (comes with the game)
|
||||
try {
|
||||
const bundled = getBundledJavaPath(JRE_DIR);
|
||||
if (bundled && fs.existsSync(bundled)) {
|
||||
console.log(`Using bundled Java: ${bundled}`);
|
||||
return bundled;
|
||||
}
|
||||
} catch (e) {
|
||||
// Bundled not available
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 2. Try javaManager's getJavaExec (handles all fallbacks)
|
||||
try {
|
||||
const javaExec = getJavaExec(JRE_DIR);
|
||||
if (javaExec && fs.existsSync(javaExec)) {
|
||||
console.log(`Using Java from javaManager: ${javaExec}`);
|
||||
return javaExec;
|
||||
}
|
||||
} catch (e) {
|
||||
// Not available
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 3. Check JAVA_HOME
|
||||
if (process.env.JAVA_HOME) {
|
||||
const javaHome = process.env.JAVA_HOME;
|
||||
const javaBin = path.join(javaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
|
||||
const javaBin = path.join(process.env.JAVA_HOME, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
|
||||
if (fs.existsSync(javaBin)) {
|
||||
console.log(`Using Java from JAVA_HOME: ${javaBin}`);
|
||||
return javaBin;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Try 'java' from PATH
|
||||
try {
|
||||
execSync('java -version 2>&1', { encoding: 'utf8' });
|
||||
console.log('Using Java from PATH');
|
||||
return 'java';
|
||||
} catch (e) {
|
||||
// Not in PATH
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
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
|
||||
*/
|
||||
@@ -961,7 +575,6 @@ class ClientPatcher {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
findServerPath(gameDir) {
|
||||
const candidates = [
|
||||
path.join(gameDir, 'Server', 'HytaleServer.jar'),
|
||||
@@ -978,9 +591,6 @@ 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 = {
|
||||
@@ -991,13 +601,9 @@ class ClientPatcher {
|
||||
|
||||
const clientPath = this.findClientPath(gameDir);
|
||||
if (clientPath) {
|
||||
if (progressCallback) {
|
||||
progressCallback('Patching client binary...', 10);
|
||||
}
|
||||
if (progressCallback) progressCallback('Patching client binary...', 10);
|
||||
results.client = await this.patchClient(clientPath, (msg, pct) => {
|
||||
if (progressCallback) {
|
||||
progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
|
||||
}
|
||||
if (progressCallback) progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
|
||||
});
|
||||
} else {
|
||||
console.warn('Could not find HytaleClient binary');
|
||||
@@ -1006,13 +612,9 @@ 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');
|
||||
@@ -1023,9 +625,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,8 @@ class UserDataBackup {
|
||||
console.log(`[UserDataBackup] Copying from ${userDataPath} to ${backupPath}...`);
|
||||
await fs.copy(userDataPath, backupPath, {
|
||||
overwrite: true,
|
||||
errorOnExist: false
|
||||
errorOnExist: false,
|
||||
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||
});
|
||||
console.log('[UserDataBackup] ✓ Backup completed successfully');
|
||||
return backupPath;
|
||||
@@ -82,7 +83,8 @@ class UserDataBackup {
|
||||
|
||||
await fs.copy(backupPath, userDataPath, {
|
||||
overwrite: true,
|
||||
errorOnExist: false
|
||||
errorOnExist: false,
|
||||
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||
});
|
||||
|
||||
console.log('UserData restore completed successfully');
|
||||
|
||||
172
backend/utils/userDataMigration.js
Normal file
172
backend/utils/userDataMigration.js
Normal file
@@ -0,0 +1,172 @@
|
||||
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
|
||||
};
|
||||
231
docs/STEAMDECK_CRASH_INVESTIGATION.md
Normal file
231
docs/STEAMDECK_CRASH_INVESTIGATION.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# 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.
|
||||
95
docs/STEAMDECK_DEBUG_COMMANDS.md
Normal file
95
docs/STEAMDECK_DEBUG_COMMANDS.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 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
|
||||
```
|
||||
9
main.js
9
main.js
@@ -5,6 +5,7 @@ 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 {
|
||||
@@ -298,6 +299,14 @@ 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 () => {
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hytale-f2p-launcher",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hytale-f2p-launcher",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.10",
|
||||
|
||||
17
package.json
17
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hytale-f2p-launcher",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.2",
|
||||
"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",
|
||||
@@ -11,7 +11,11 @@
|
||||
"build:win": "electron-builder --win",
|
||||
"build:linux": "electron-builder --linux",
|
||||
"build:mac": "electron-builder --mac",
|
||||
"build:all": "electron-builder --win --linux --mac"
|
||||
"build:all": "electron-builder --win --linux --mac",
|
||||
"build:arch": "electron-builder --linux dir",
|
||||
"build:appimage": "electron-builder --linux AppImage --publish never",
|
||||
"build:deb": "electron-builder --linux deb --publish never",
|
||||
"build:rpm": "electron-builder --linux rpm --publish never"
|
||||
},
|
||||
"keywords": [
|
||||
"hytale",
|
||||
@@ -82,7 +86,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "icon.ico"
|
||||
"icon": "build/icon.ico"
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
@@ -106,13 +110,6 @@
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "pacman",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "build/icon.png",
|
||||
|
||||
Reference in New Issue
Block a user