mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 11:41:49 -03:00
Compare commits
43 Commits
v2.2.1
...
fix/patche
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab6f932245 | ||
|
|
6333263ef9 | ||
|
|
654deca933 | ||
|
|
aab67e8e28 | ||
|
|
3953827f4a | ||
|
|
81d1e7c113 | ||
|
|
d8f90bd1ff | ||
|
|
50491abc69 | ||
|
|
c92c5bec3c | ||
|
|
73f67f2fec | ||
|
|
2582d9b6d1 | ||
|
|
e56b12cd72 | ||
|
|
3edee4b4eb | ||
|
|
e5fec7c326 | ||
|
|
7d2672b684 | ||
|
|
01823729ec | ||
|
|
639a2ab1b5 | ||
|
|
6b76eb365e | ||
|
|
6fa933fece | ||
|
|
e7023dcf95 | ||
|
|
f4d966ee65 | ||
|
|
ca835a868b | ||
|
|
3a1b6039d0 | ||
|
|
7828454631 | ||
|
|
cc1c6c334c | ||
|
|
081ac926e3 | ||
|
|
75a450c9ec | ||
|
|
e426690632 | ||
|
|
78f76afe0a | ||
|
|
131de1dcd7 | ||
|
|
b39877f561 | ||
|
|
0b1b448cce | ||
|
|
aed00cd067 | ||
|
|
eff6fcd520 | ||
|
|
94d4586b97 | ||
|
|
20faf36b37 | ||
|
|
375b422c73 | ||
|
|
b668bdb45a | ||
|
|
653d4429ed | ||
|
|
17e15c17f0 | ||
|
|
b99b22e8bf | ||
|
|
9303c17e57 | ||
|
|
615ee5cadc |
18
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
18
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -55,6 +55,15 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
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
|
- type: dropdown
|
||||||
id: os
|
id: os
|
||||||
attributes:
|
attributes:
|
||||||
@@ -71,6 +80,15 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
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
|
- type: textarea
|
||||||
id: additional
|
id: additional
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -39,7 +39,7 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: screenshots
|
- type: textarea
|
||||||
id: screenshots
|
id: screenshots
|
||||||
attributes:
|
attributes:
|
||||||
label: Screenshots (Optional)
|
label: Screenshots (Optional)
|
||||||
|
|||||||
9
.github/ISSUE_TEMPLATE/support_request.yml
vendored
9
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -28,6 +28,15 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
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
|
- type: input
|
||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const i18n = (() => {
|
|||||||
{ code: 'en', name: 'English' },
|
{ code: 'en', name: 'English' },
|
||||||
{ code: 'es-ES', name: 'Español (España)' },
|
{ code: 'es-ES', name: 'Español (España)' },
|
||||||
{ code: 'pt-BR', name: 'Portuguese (Brazil)' },
|
{ 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
|
// Load single language file
|
||||||
|
|||||||
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!"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
PKGBUILD
12
PKGBUILD
@@ -14,15 +14,15 @@ source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop")
|
|||||||
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "$_pkgname-$pkgver"
|
cd "$pkgname-$pkgver"
|
||||||
npm ci
|
npm ci
|
||||||
npm run build:arch
|
npm run build:arch
|
||||||
}
|
}
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cd "$_pkgname-$pkgver"
|
cd "$pkgname-$pkgver"
|
||||||
install -d "$pkgdir/opt/$_pkgname"
|
install -d "$pkgdir/opt/$pkgname"
|
||||||
cp -r dist/linux-unpacked/* "$pkgdir/opt/$_pkgname"
|
cp -r dist/linux-unpacked/* "$pkgdir/opt/$pkgname"
|
||||||
install -Dm644 "$srcdir/$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
|
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"
|
install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$pkgname.png"
|
||||||
}
|
}
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -308,20 +308,21 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
|||||||
## 📋 Changelog
|
## 📋 Changelog
|
||||||
|
|
||||||
### 🆕 v2.1.1
|
### 🆕 v2.1.1
|
||||||
- 🛠️ **Fix EPERM** Issue
|
- 🛠️ **Fix Bug EPERM**: EPERM or Error Permission in creating/removing process in reinstalling is now fixed.
|
||||||
- 🅰️ **Adds Better Arch Build**
|
- 🅰️ **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>
|
<details>
|
||||||
<summary>Click here to see older Changelogs</summary>
|
<summary>Click here to see older Changelogs</summary>
|
||||||
|
|
||||||
### 🆕 v2.1.0
|
### 🔄 v2.1.0
|
||||||
- 🚨 **Auto-Retry Downloads and Auto-Patch Files** —
|
- 🚨 **Auto-Retry Downloads and Auto-Patch Files** —
|
||||||
- ⚡ **Hardware Acceleration** —
|
- ⚡ **Hardware Acceleration** —
|
||||||
- 🔎 **Browse CurseForge Mods** — Browsing mods now easier with our dedicated CurseForge API Key.
|
- 🔎 **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.
|
- 🌎 **Fixes and Release New Translation** — Fixed 🇪🇸 🇧🇷 and added more translation for current build. Turkish 🇹🇷 language now added.
|
||||||
|
|
||||||
### 🆕 v2.0.2b *(Minor Update: Performance & Utilities)*
|
### 🔄 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.**
|
- 🌎 **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*.
|
- 💻 **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.
|
- 👨💻 **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();
|
const DEFAULT_APP_DIR = getAppDir();
|
||||||
|
|
||||||
function getResolvedAppDir(customPath) {
|
function getResolvedAppDir(customPath) {
|
||||||
@@ -218,20 +233,8 @@ async function getModsPath(customInstallPath = null) {
|
|||||||
|
|
||||||
function getProfilesDir(customInstallPath = null) {
|
function getProfilesDir(customInstallPath = null) {
|
||||||
try {
|
try {
|
||||||
// get UserData path
|
// NEW 2.1.2: Use centralized UserData location
|
||||||
let installPath = customInstallPath;
|
const userDataPath = getHytaleSavesDir();
|
||||||
if (!installPath) {
|
|
||||||
const configFile = path.join(DEFAULT_APP_DIR, 'config.json');
|
|
||||||
if (fs.existsSync(configFile)) {
|
|
||||||
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
||||||
installPath = config.installPath || '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!installPath) installPath = getAppDir();
|
|
||||||
|
|
||||||
const branch = loadVersionBranch();
|
|
||||||
const gameLatest = path.join(installPath, branch, 'package', 'game', 'latest');
|
|
||||||
const userDataPath = findUserDataPath(gameLatest);
|
|
||||||
const profilesDir = path.join(userDataPath, 'Profiles');
|
const profilesDir = path.join(userDataPath, 'Profiles');
|
||||||
|
|
||||||
if (!fs.existsSync(profilesDir)) {
|
if (!fs.existsSync(profilesDir)) {
|
||||||
@@ -247,6 +250,7 @@ function getProfilesDir(customInstallPath = null) {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getAppDir,
|
getAppDir,
|
||||||
|
getHytaleSavesDir,
|
||||||
getResolvedAppDir,
|
getResolvedAppDir,
|
||||||
expandHome,
|
expandHome,
|
||||||
APP_DIR,
|
APP_DIR,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA
|
|||||||
const { getLatestClientVersion } = require('../services/versionManager');
|
const { getLatestClientVersion } = require('../services/versionManager');
|
||||||
const { updateGameFiles } = require('./gameManager');
|
const { updateGameFiles } = require('./gameManager');
|
||||||
const { syncModsForCurrentProfile } = require('./modManager');
|
const { syncModsForCurrentProfile } = require('./modManager');
|
||||||
|
const { getUserDataPath } = require('../utils/userDataMigration');
|
||||||
|
|
||||||
// Client patcher for custom auth server (sanasol.ws)
|
// Client patcher for custom auth server (sanasol.ws)
|
||||||
let clientPatcher = null;
|
let clientPatcher = null;
|
||||||
@@ -106,7 +107,9 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
|||||||
const customAppDir = getResolvedAppDir(installPathOverride);
|
const customAppDir = getResolvedAppDir(installPathOverride);
|
||||||
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||||
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', '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;
|
const gameLatest = customGameDir;
|
||||||
let clientPath = findClientPath(gameLatest);
|
let clientPath = findClientPath(gameLatest);
|
||||||
@@ -282,6 +285,32 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
||||||
Object.assign(env, gpuEnv);
|
Object.assign(env, gpuEnv);
|
||||||
|
|
||||||
|
// Linux memory allocator fixes for "free(): invalid pointer" crashes
|
||||||
|
// on Steam Deck (glibc 2.41) and Ubuntu LTS
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
// Option 1: Disable glibc heap validation
|
||||||
|
env.MALLOC_CHECK_ = '0';
|
||||||
|
|
||||||
|
// Option 2: Try to use jemalloc if available (more robust allocator)
|
||||||
|
// User can set HYTALE_USE_JEMALLOC=1 to enable
|
||||||
|
if (process.env.HYTALE_USE_JEMALLOC === '1') {
|
||||||
|
const jemalloc = require('fs').existsSync('/usr/lib/libjemalloc.so.2')
|
||||||
|
? '/usr/lib/libjemalloc.so.2'
|
||||||
|
: require('fs').existsSync('/usr/lib/x86_64-linux-gnu/libjemalloc.so.2')
|
||||||
|
? '/usr/lib/x86_64-linux-gnu/libjemalloc.so.2'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (jemalloc) {
|
||||||
|
env.LD_PRELOAD = jemalloc + (env.LD_PRELOAD ? ':' + env.LD_PRELOAD : '');
|
||||||
|
console.log(`Linux: Using jemalloc allocator (${jemalloc})`);
|
||||||
|
} else {
|
||||||
|
console.log('Linux: jemalloc not found, using glibc with MALLOC_CHECK_=0');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Linux: Using glibc with MALLOC_CHECK_=0 (set HYTALE_USE_JEMALLOC=1 to try jemalloc)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let spawnOptions = {
|
let spawnOptions = {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const { installButler } = require('./butlerManager');
|
|||||||
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
||||||
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config');
|
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config');
|
||||||
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
|
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) {
|
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
||||||
const osName = getOS();
|
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) {
|
async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, cacheDir = CACHE_DIR, branchOverride = null) {
|
||||||
let tempUpdateDir;
|
let tempUpdateDir;
|
||||||
let backupPath = null;
|
|
||||||
const branch = branchOverride || loadVersionBranch();
|
const branch = branchOverride || loadVersionBranch();
|
||||||
const installPath = path.dirname(path.dirname(path.dirname(path.dirname(gameDir))));
|
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 config = loadConfig();
|
||||||
const hasVersionConfig = !!(config.version_client && config.version_branch);
|
const oldBranch = config.version_branch || 'release';
|
||||||
const oldBranch = config.version_branch || 'release'; // L'ancienne branche pour le backup
|
|
||||||
console.log(`[UpdateGameFiles] hasVersionConfig: ${hasVersionConfig}`);
|
|
||||||
console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`);
|
console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (progressCallback) {
|
// NEW 2.1.2: Ensure UserData migration to centralized location
|
||||||
progressCallback('Backing up user data...', 5, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backup UserData AVANT de télécharger/installer (critical for same-branch updates)
|
|
||||||
try {
|
try {
|
||||||
console.log(`[UpdateGameFiles] Attempting to backup UserData from old branch: ${oldBranch}`);
|
console.log('[UpdateGameFiles] Ensuring UserData migration...');
|
||||||
backupPath = await userDataBackup.backupUserData(installPath, oldBranch, hasVersionConfig);
|
const migrationResult = await migrateUserDataToCentralized();
|
||||||
if (backupPath) {
|
if (migrationResult.migrated) {
|
||||||
console.log(`[UpdateGameFiles] ✓ UserData backed up from ${oldBranch}: ${backupPath}`);
|
console.log('[UpdateGameFiles] ✓ UserData migrated to centralized location');
|
||||||
|
} else if (migrationResult.alreadyMigrated) {
|
||||||
|
console.log('[UpdateGameFiles] ✓ UserData already in centralized location');
|
||||||
}
|
}
|
||||||
} catch (backupError) {
|
} catch (migrationError) {
|
||||||
console.warn('[UpdateGameFiles] ✗ UserData backup failed:', backupError.message);
|
console.warn('[UpdateGameFiles] UserData migration warning:', migrationError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -390,31 +384,9 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
|
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
|
||||||
console.log('Logo@2x.png update result after update:', logoResult);
|
console.log('Logo@2x.png update result after update:', logoResult);
|
||||||
|
|
||||||
// Ensure UserData directory exists
|
// NEW 2.1.2: No longer create UserData in game installation
|
||||||
const userDataDir = path.join(gameDir, 'Client', 'UserData');
|
// UserData is now in centralized location (getUserDataPath())
|
||||||
if (!fs.existsSync(userDataDir)) {
|
console.log('[UpdateGameFiles] UserData is managed in centralized location');
|
||||||
console.log(`[UpdateGameFiles] Creating UserData directory at: ${userDataDir}`);
|
|
||||||
fs.mkdirSync(userDataDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) {
|
|
||||||
progressCallback('Restoring user data...', 90, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore UserData using new system
|
|
||||||
if (backupPath) {
|
|
||||||
try {
|
|
||||||
console.log(`[UpdateGameFiles] Restoring UserData from ${oldBranch} to ${branch}`);
|
|
||||||
console.log(`[UpdateGameFiles] Source backup: ${backupPath}`);
|
|
||||||
await userDataBackup.restoreUserData(backupPath, installPath, branch);
|
|
||||||
await userDataBackup.cleanupBackup(backupPath);
|
|
||||||
console.log(`[UpdateGameFiles] ✓ UserData migrated successfully from ${oldBranch} to ${branch}`);
|
|
||||||
} catch (restoreError) {
|
|
||||||
console.warn('[UpdateGameFiles] ✗ UserData restore failed:', restoreError.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('[UpdateGameFiles] No backup to restore, empty UserData folder created');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Game files updated successfully to version: ${newVersion}`);
|
console.log(`Game files updated successfully to version: ${newVersion}`);
|
||||||
|
|
||||||
@@ -434,15 +406,6 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating game files:', 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)) {
|
if (tempUpdateDir && fs.existsSync(tempUpdateDir)) {
|
||||||
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -470,28 +433,18 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
const customToolsDir = path.join(customAppDir, 'butler');
|
const customToolsDir = path.join(customAppDir, 'butler');
|
||||||
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||||
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', '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 {
|
try {
|
||||||
console.log(`[InstallGame] Attempting UserData backup (hasVersionConfig: ${hasVersionConfig})...`);
|
console.log('[InstallGame] Ensuring UserData migration...');
|
||||||
backupPath = await userDataBackup.backupUserData(customAppDir, branch, hasVersionConfig);
|
const migrationResult = await migrateUserDataToCentralized();
|
||||||
if (backupPath) {
|
if (migrationResult.migrated) {
|
||||||
console.log(`[InstallGame] ✓ UserData backed up to: ${backupPath}`);
|
console.log('[InstallGame] ✓ UserData migrated to centralized location');
|
||||||
|
} else if (migrationResult.alreadyMigrated) {
|
||||||
|
console.log('[InstallGame] ✓ UserData already in centralized location');
|
||||||
}
|
}
|
||||||
} catch (backupError) {
|
} catch (migrationError) {
|
||||||
console.warn('[InstallGame] ✗ UserData backup failed:', backupError.message);
|
console.warn('[InstallGame] UserData migration warning:', migrationError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[customAppDir, customCacheDir, customToolsDir].forEach(dir => {
|
[customAppDir, customCacheDir, customToolsDir].forEach(dir => {
|
||||||
@@ -500,10 +453,6 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!fs.existsSync(userDataDir)) {
|
|
||||||
fs.mkdirSync(userDataDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
saveUsername(playerName);
|
saveUsername(playerName);
|
||||||
if (installPathOverride) {
|
if (installPathOverride) {
|
||||||
saveInstallPath(installPathOverride);
|
saveInstallPath(installPathOverride);
|
||||||
@@ -595,29 +544,9 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
|
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
|
||||||
console.log('Logo@2x.png update result after installation:', logoResult);
|
console.log('Logo@2x.png update result after installation:', logoResult);
|
||||||
|
|
||||||
// Ensure UserData directory exists
|
// NEW 2.1.2: No longer create UserData in game installation
|
||||||
if (!fs.existsSync(userDataDir)) {
|
// UserData is managed in centralized location (getUserDataPath())
|
||||||
console.log(`[InstallGame] Creating UserData directory at: ${userDataDir}`);
|
console.log('[InstallGame] UserData is managed in centralized location');
|
||||||
fs.mkdirSync(userDataDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore UserData from backup if exists
|
|
||||||
if (backupPath) {
|
|
||||||
if (progressCallback) {
|
|
||||||
progressCallback('Restoring UserData...', 95, null, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log(`[InstallGame] Restoring UserData from: ${backupPath}`);
|
|
||||||
await userDataBackup.restoreUserData(backupPath, customAppDir, branch);
|
|
||||||
await userDataBackup.cleanupBackup(backupPath);
|
|
||||||
console.log('[InstallGame] ✓ UserData restored successfully');
|
|
||||||
} catch (restoreError) {
|
|
||||||
console.warn('[InstallGame] ✗ UserData restore failed:', restoreError.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('[InstallGame] No backup to restore, empty UserData folder created');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Installation complete', 100, null, null, null);
|
progressCallback('Installation complete', 100, null, null, null);
|
||||||
|
|||||||
@@ -147,8 +147,9 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace bytes in buffer - only overwrites the length of new bytes
|
* Replace bytes in buffer with null-padding for shorter replacements
|
||||||
* Prevents offset corruption by not expanding the replacement
|
* When new pattern is shorter than old, pads with 0x00 to prevent leftover bytes
|
||||||
|
* that can cause memory corruption (free(): invalid pointer) on some systems
|
||||||
*/
|
*/
|
||||||
replaceBytes(buffer, oldBytes, newBytes) {
|
replaceBytes(buffer, oldBytes, newBytes) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -162,7 +163,20 @@ class ClientPatcher {
|
|||||||
const positions = this.findAllOccurrences(result, oldBytes);
|
const positions = this.findAllOccurrences(result, oldBytes);
|
||||||
|
|
||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
// Only overwrite the length of the new bytes
|
// Log offset and surrounding bytes for debugging
|
||||||
|
const before = result.slice(Math.max(0, pos - 8), pos);
|
||||||
|
const after = result.slice(pos + oldBytes.length, Math.min(result.length, pos + oldBytes.length + 8));
|
||||||
|
console.log(` Patching at offset 0x${pos.toString(16)} (${pos})`);
|
||||||
|
console.log(` Before: ${before.toString('hex')}`);
|
||||||
|
console.log(` Old pattern: ${oldBytes.slice(0, 20).toString('hex')}${oldBytes.length > 20 ? '...' : ''}`);
|
||||||
|
console.log(` After: ${after.toString('hex')}`);
|
||||||
|
|
||||||
|
// First fill the entire old pattern region with zeros
|
||||||
|
// This prevents leftover bytes from causing memory corruption
|
||||||
|
if (newBytes.length < oldBytes.length) {
|
||||||
|
result.fill(0x00, pos, pos + oldBytes.length);
|
||||||
|
}
|
||||||
|
// Then write the new bytes
|
||||||
newBytes.copy(result, pos);
|
newBytes.copy(result, pos);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
@@ -170,6 +184,65 @@ class ClientPatcher {
|
|||||||
return { buffer: result, count };
|
return { buffer: result, count };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace bytes with skip/limit control (for debugging)
|
||||||
|
* HYTALE_PATCH_SKIP: comma-separated indices to skip (e.g., "0,2" skips 1st and 3rd)
|
||||||
|
* HYTALE_PATCH_LIMIT: max number of patches to apply
|
||||||
|
*/
|
||||||
|
replaceBytesLimited(buffer, oldBytes, newBytes, limit) {
|
||||||
|
let count = 0;
|
||||||
|
const result = Buffer.from(buffer);
|
||||||
|
|
||||||
|
if (newBytes.length > oldBytes.length) {
|
||||||
|
console.warn(` Warning: New pattern (${newBytes.length}) longer than old (${oldBytes.length}), skipping`);
|
||||||
|
return { buffer: result, count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse skip list from env
|
||||||
|
const skipIndices = (process.env.HYTALE_PATCH_SKIP || '')
|
||||||
|
.split(',')
|
||||||
|
.filter(s => s.trim())
|
||||||
|
.map(s => parseInt(s.trim(), 10));
|
||||||
|
if (skipIndices.length > 0) {
|
||||||
|
console.log(` Skip indices: ${skipIndices.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = this.findAllOccurrences(result, oldBytes);
|
||||||
|
|
||||||
|
let patchedCount = 0;
|
||||||
|
for (let i = 0; i < positions.length; i++) {
|
||||||
|
const pos = positions[i];
|
||||||
|
|
||||||
|
// Log offset and surrounding bytes for debugging
|
||||||
|
const before = result.slice(Math.max(0, pos - 8), pos);
|
||||||
|
const after = result.slice(pos + oldBytes.length, Math.min(result.length, pos + oldBytes.length + 8));
|
||||||
|
|
||||||
|
if (skipIndices.includes(i)) {
|
||||||
|
console.log(` [${i}] Skipping offset 0x${pos.toString(16)} (in skip list)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patchedCount >= limit) {
|
||||||
|
console.log(` [${i}] Skipping offset 0x${pos.toString(16)} (limit reached)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` [${i}] Patching at offset 0x${pos.toString(16)} (${pos})`);
|
||||||
|
console.log(` Before: ${before.toString('hex')}`);
|
||||||
|
console.log(` Old pattern: ${oldBytes.slice(0, 20).toString('hex')}${oldBytes.length > 20 ? '...' : ''}`);
|
||||||
|
console.log(` After: ${after.toString('hex')}`);
|
||||||
|
|
||||||
|
if (newBytes.length < oldBytes.length) {
|
||||||
|
result.fill(0x00, pos, pos + oldBytes.length);
|
||||||
|
}
|
||||||
|
newBytes.copy(result, pos);
|
||||||
|
patchedCount++;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { buffer: result, count };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UTF-8 domain replacement for Java JAR files.
|
* UTF-8 domain replacement for Java JAR files.
|
||||||
* Java stores strings in UTF-8 format in the constant pool.
|
* Java stores strings in UTF-8 format in the constant pool.
|
||||||
@@ -197,11 +270,19 @@ class ClientPatcher {
|
|||||||
* .NET AOT stores some strings in various formats:
|
* .NET AOT stores some strings in various formats:
|
||||||
* - Standard UTF-16LE (each char is 2 bytes with \x00 high byte)
|
* - Standard UTF-16LE (each char is 2 bytes with \x00 high byte)
|
||||||
* - Length-prefixed where last char may have metadata byte instead of \x00
|
* - Length-prefixed where last char may have metadata byte instead of \x00
|
||||||
|
*
|
||||||
|
* IMPORTANT: newDomain must be same length or shorter than oldDomain to avoid buffer overflow
|
||||||
*/
|
*/
|
||||||
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
|
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const result = Buffer.from(data);
|
const result = Buffer.from(data);
|
||||||
|
|
||||||
|
// Safety check: new domain must not be longer than old
|
||||||
|
if (newDomain.length > oldDomain.length) {
|
||||||
|
console.warn(` Warning: New domain (${newDomain.length} chars) longer than old (${oldDomain.length} chars), skipping smart replacement`);
|
||||||
|
return { buffer: result, count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1));
|
const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1));
|
||||||
const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1));
|
const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1));
|
||||||
|
|
||||||
@@ -217,6 +298,11 @@ class ClientPatcher {
|
|||||||
const lastCharFirstByte = result[lastCharPos];
|
const lastCharFirstByte = result[lastCharPos];
|
||||||
|
|
||||||
if (lastCharFirstByte === oldLastCharByte) {
|
if (lastCharFirstByte === oldLastCharByte) {
|
||||||
|
// Zero-fill the old region first if new is shorter
|
||||||
|
if (newUtf16NoLast.length < oldUtf16NoLast.length) {
|
||||||
|
result.fill(0x00, pos, pos + oldUtf16NoLast.length);
|
||||||
|
}
|
||||||
|
|
||||||
newUtf16NoLast.copy(result, pos);
|
newUtf16NoLast.copy(result, pos);
|
||||||
|
|
||||||
result[lastCharPos] = newLastCharByte;
|
result[lastCharPos] = newLastCharByte;
|
||||||
@@ -239,6 +325,10 @@ class ClientPatcher {
|
|||||||
/**
|
/**
|
||||||
* Apply all domain patches using length-prefixed format
|
* Apply all domain patches using length-prefixed format
|
||||||
* This is the main patching method for variable-length domains
|
* This is the main patching method for variable-length domains
|
||||||
|
*
|
||||||
|
* Debug env vars:
|
||||||
|
* HYTALE_SKIP_SENTRY_PATCH=1 - Skip sentry URL patch (biggest size change)
|
||||||
|
* HYTALE_SKIP_SUBDOMAIN_PATCH=1 - Skip subdomain prefix patches
|
||||||
*/
|
*/
|
||||||
applyDomainPatches(data, domain, protocol = 'https://') {
|
applyDomainPatches(data, domain, protocol = 'https://') {
|
||||||
let result = Buffer.from(data);
|
let result = Buffer.from(data);
|
||||||
@@ -247,7 +337,10 @@ class ClientPatcher {
|
|||||||
|
|
||||||
console.log(` Patching strategy: ${strategy.description}`);
|
console.log(` Patching strategy: ${strategy.description}`);
|
||||||
|
|
||||||
// 1. Patch telemetry/sentry URL
|
// 1. Patch telemetry/sentry URL (skip if debugging)
|
||||||
|
if (process.env.HYTALE_SKIP_SENTRY_PATCH === '1') {
|
||||||
|
console.log(` Skipping sentry patch (HYTALE_SKIP_SENTRY_PATCH=1)`);
|
||||||
|
} else {
|
||||||
const oldSentry = 'https://ca900df42fcf57d4dd8401a86ddd7da2@sentry.hytale.com/2';
|
const oldSentry = 'https://ca900df42fcf57d4dd8401a86ddd7da2@sentry.hytale.com/2';
|
||||||
const newSentry = `${protocol}t@${domain}/2`;
|
const newSentry = `${protocol}t@${domain}/2`;
|
||||||
|
|
||||||
@@ -262,21 +355,46 @@ class ClientPatcher {
|
|||||||
console.log(` Replaced ${sentryResult.count} sentry occurrence(s)`);
|
console.log(` Replaced ${sentryResult.count} sentry occurrence(s)`);
|
||||||
totalCount += sentryResult.count;
|
totalCount += sentryResult.count;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Patch main domain (hytale.com -> mainDomain)
|
// 2. Patch main domain (hytale.com -> mainDomain)
|
||||||
|
// Try length-prefixed format first, then fall back to pure UTF-16LE
|
||||||
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
|
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
|
||||||
const domainResult = this.replaceBytes(
|
|
||||||
|
// Check for HYTALE_PATCH_MODE env var to test different formats
|
||||||
|
const patchMode = process.env.HYTALE_PATCH_MODE || 'length-prefixed';
|
||||||
|
console.log(` Patch mode: ${patchMode}`);
|
||||||
|
|
||||||
|
let domainResult;
|
||||||
|
if (patchMode === 'utf16le') {
|
||||||
|
// Pure UTF-16LE replacement (no length prefix)
|
||||||
|
const oldUtf16 = this.stringToUtf16LE(ORIGINAL_DOMAIN);
|
||||||
|
const newUtf16 = this.stringToUtf16LE(strategy.mainDomain);
|
||||||
|
console.log(` UTF-16LE: old=${oldUtf16.length} bytes, new=${newUtf16.length} bytes`);
|
||||||
|
|
||||||
|
// HYTALE_PATCH_LIMIT: only patch first N occurrences (for debugging)
|
||||||
|
const patchLimit = parseInt(process.env.HYTALE_PATCH_LIMIT || '999', 10);
|
||||||
|
console.log(` Patch limit: ${patchLimit}`);
|
||||||
|
domainResult = this.replaceBytesLimited(result, oldUtf16, newUtf16, patchLimit);
|
||||||
|
} else {
|
||||||
|
// Length-prefixed format (default)
|
||||||
|
domainResult = this.replaceBytes(
|
||||||
result,
|
result,
|
||||||
this.stringToLengthPrefixed(ORIGINAL_DOMAIN),
|
this.stringToLengthPrefixed(ORIGINAL_DOMAIN),
|
||||||
this.stringToLengthPrefixed(strategy.mainDomain)
|
this.stringToLengthPrefixed(strategy.mainDomain)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
result = domainResult.buffer;
|
result = domainResult.buffer;
|
||||||
if (domainResult.count > 0) {
|
if (domainResult.count > 0) {
|
||||||
console.log(` Replaced ${domainResult.count} domain occurrence(s)`);
|
console.log(` Replaced ${domainResult.count} domain occurrence(s)`);
|
||||||
totalCount += domainResult.count;
|
totalCount += domainResult.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Patch subdomain prefixes
|
// 3. Patch subdomain prefixes (skip if debugging)
|
||||||
|
if (process.env.HYTALE_SKIP_SUBDOMAIN_PATCH === '1') {
|
||||||
|
console.log(` Skipping subdomain patches (HYTALE_SKIP_SUBDOMAIN_PATCH=1)`);
|
||||||
|
} else {
|
||||||
const subdomains = ['https://tools.', 'https://sessions.', 'https://account-data.', 'https://telemetry.'];
|
const subdomains = ['https://tools.', 'https://sessions.', 'https://account-data.', 'https://telemetry.'];
|
||||||
const newSubdomainPrefix = protocol + strategy.subdomainPrefix;
|
const newSubdomainPrefix = protocol + strategy.subdomainPrefix;
|
||||||
|
|
||||||
@@ -293,43 +411,19 @@ class ClientPatcher {
|
|||||||
totalCount += subResult.count;
|
totalCount += subResult.count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { buffer: result, count: totalCount };
|
return { buffer: result, count: totalCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7
|
* Patch Discord invite URLs - DISABLED
|
||||||
|
* Was causing buffer overflow crashes on Steam Deck/Ubuntu LTS
|
||||||
|
* The Discord URL in the game doesn't affect F2P functionality anyway
|
||||||
*/
|
*/
|
||||||
patchDiscordUrl(data) {
|
patchDiscordUrl(data) {
|
||||||
let count = 0;
|
// Disabled - no practical effect and was causing memory corruption
|
||||||
const result = Buffer.from(data);
|
return { buffer: Buffer.from(data), count: 0 };
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -483,6 +577,14 @@ class ClientPatcher {
|
|||||||
progressCallback('Patching domain references...', 50);
|
progressCallback('Patching domain references...', 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HYTALE_NOOP_TEST: Just read and write binary without any changes
|
||||||
|
if (process.env.HYTALE_NOOP_TEST === '1') {
|
||||||
|
console.log('NOOP TEST: Writing binary without modifications...');
|
||||||
|
fs.writeFileSync(clientPath, data);
|
||||||
|
this.markAsPatched(clientPath);
|
||||||
|
return { success: true, patchCount: 0, noop: true };
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Applying domain patches (length-prefixed format)...');
|
console.log('Applying domain patches (length-prefixed format)...');
|
||||||
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
||||||
|
|
||||||
@@ -490,6 +592,17 @@ class ClientPatcher {
|
|||||||
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
|
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
|
||||||
|
|
||||||
if (count === 0 && discordCount === 0) {
|
if (count === 0 && discordCount === 0) {
|
||||||
|
// Check if we're in debug mode with skip - don't fallback if intentionally skipping
|
||||||
|
const hasSkipList = (process.env.HYTALE_PATCH_SKIP || '').trim().length > 0;
|
||||||
|
const noLegacyFallback = process.env.HYTALE_NO_LEGACY_FALLBACK === '1';
|
||||||
|
|
||||||
|
if (hasSkipList || noLegacyFallback) {
|
||||||
|
console.log('No occurrences patched (skip list active or legacy fallback disabled)');
|
||||||
|
fs.writeFileSync(clientPath, patchedData);
|
||||||
|
this.markAsPatched(clientPath);
|
||||||
|
return { success: true, patchCount: 0, skipped: true };
|
||||||
|
}
|
||||||
|
|
||||||
console.log('No occurrences found - trying legacy UTF-16LE format...');
|
console.log('No occurrences found - trying legacy UTF-16LE format...');
|
||||||
|
|
||||||
// Fallback to legacy patching for older binary formats
|
// Fallback to legacy patching for older binary formats
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ class UserDataBackup {
|
|||||||
console.log(`[UserDataBackup] Copying from ${userDataPath} to ${backupPath}...`);
|
console.log(`[UserDataBackup] Copying from ${userDataPath} to ${backupPath}...`);
|
||||||
await fs.copy(userDataPath, backupPath, {
|
await fs.copy(userDataPath, backupPath, {
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
errorOnExist: false
|
errorOnExist: false,
|
||||||
|
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||||
});
|
});
|
||||||
console.log('[UserDataBackup] ✓ Backup completed successfully');
|
console.log('[UserDataBackup] ✓ Backup completed successfully');
|
||||||
return backupPath;
|
return backupPath;
|
||||||
@@ -82,7 +83,8 @@ class UserDataBackup {
|
|||||||
|
|
||||||
await fs.copy(backupPath, userDataPath, {
|
await fs.copy(backupPath, userDataPath, {
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
errorOnExist: false
|
errorOnExist: false,
|
||||||
|
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('UserData restore completed successfully');
|
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
|
||||||
|
};
|
||||||
9
main.js
9
main.js
@@ -5,6 +5,7 @@ const { autoUpdater } = require('electron-updater');
|
|||||||
const fs = require('fs');
|
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 { 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 { retryPWRDownload } = require('./backend/managers/gameManager');
|
||||||
|
const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration');
|
||||||
|
|
||||||
// Handle Hardware Acceleration
|
// Handle Hardware Acceleration
|
||||||
try {
|
try {
|
||||||
@@ -298,6 +299,14 @@ app.whenReady().then(async () => {
|
|||||||
// Initialize Profile Manager (runs migration if needed)
|
// Initialize Profile Manager (runs migration if needed)
|
||||||
profileManager.init();
|
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();
|
createSplashScreen();
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
||||||
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
|
|||||||
Reference in New Issue
Block a user