Compare commits

...

94 Commits

Author SHA1 Message Date
sanasol
d0831b3b83 fix(ci): use APPLE_APP_SPECIFIC_PASSWORD for notarization 2026-02-03 03:03:51 +01:00
sanasol
5cf9fa3af4 fix(ci): switch to built-in electron-builder notarization
- Remove custom afterSign hook (scripts/notarize.js)
- Enable built-in notarization with "notarize": true
- Use APPLE_ID_PASSWORD env var for electron-builder
- Restore full build (dmg + zip) to test blockmap

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 03:00:30 +01:00
sanasol
ee4909cc72 fix(ci): build only DMG for macOS to avoid blockmap hang
- Skip zip target, only build DMG
- Blockmap generation for universal+zip was hanging indefinitely
- DMG alone should complete faster

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 02:49:36 +01:00
sanasol
9d63e6e971 fix(ci): revert to universal macOS build with single notarization
- Replace separate ARM64 and x64 builds with single universal build
- Use --universal flag for fat binary (both archs in one app)
- Update package.json mac target to use "universal" arch
- Single notarization instead of double (fixes duplicate notarize calls)
- Simplify workflow by removing separate macOS release jobs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 02:28:11 +01:00
sanasol
153868fb87 fix(macos): improve notarization with timeout and graceful failure
Changes:
- Add 30 minute timeout for notarization (fail fast)
- Add SKIP_NOTARIZE=true env var to skip notarization entirely
- Don't fail build if notarization fails (app still code-signed)
- Add NOTARIZE_FAIL_ON_ERROR=true to fail build on notarization error
- Add forceCodeSigning, strictVerify, type=distribution to mac config
- Disable electron-builder built-in notarize (using custom script)

This prevents CI from hanging forever waiting for Apple's notarization
service and reduces wasted GitHub Actions minutes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 03:00:47 +01:00
sanasol
743d7f2b7c fix(ci): use macos-latest for ARM64 builds 2026-01-31 20:02:07 +01:00
sanasol
759a7089c4 fix(ci): use macos-15-large for Intel x64 builds (macos-13 retired) 2026-01-31 20:00:13 +01:00
sanasol
0a5cc3f6d7 feat(ci): separate macOS arm64 and x64 builds with individual code signing
Changes:
- Split macOS build into two separate jobs: build-macos-arm64 and build-macos-x64
- ARM64 builds on macos-14 (M1 runner) for native Apple Silicon builds
- x64 builds on macos-13 (Intel runner) for native Intel builds
- Each build has its own code signing and notarization process
- Artifacts renamed with -arm64 and -x64 suffixes for clarity
- Separate release jobs for each architecture
- Updated package.json mac targets from "universal" to ["arm64", "x64"]

This fixes code signing issues when building universal binaries and allows
faster parallel builds for each architecture.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:58:35 +01:00
sanasol
499d9a5a6d Merge sanasol/develop into develop 2026-01-31 19:54:46 +01:00
Fazri Gading
7b2acd49b6 chore: sync package-lock with package.json 2026-01-31 22:58:02 +08:00
Fazri Gading
7b9951e72d Update release.yml 2026-01-31 20:34:57 +08:00
Fazri Gading
e81a0167c1 Merge pull request #246 from amiayweb/fix/v2.2.0-failed-release
Fix/v2.2.0 failed release
2026-01-31 20:32:31 +08:00
Fazri Gading
50c04b64df Merge branch 'develop' into fix/v2.2.0-failed-release 2026-01-31 20:32:15 +08:00
Fazri Gading
28e5fa35e1 update package-lock.json 2026-01-31 20:05:33 +08:00
Fazri Gading
52e7eafe0b fix: redo package.json arch 2026-01-31 19:22:09 +08:00
Fazri Gading
3de5c2eaa3 fix: removed arm64 flags 2026-01-31 19:19:34 +08:00
Fazri Gading
9c9b71bd4c Merge pull request #244 from amiayweb/fix/x64-appimage-issue
fix: preserves arch x64 on linux target for #242
2026-01-31 18:53:23 +08:00
Fazri Gading
c4bb15ce91 fix: preserves arch x64 on linux target for #242 2026-01-31 18:19:39 +08:00
Fazri Gading
5147e1856f fix: preserves arch x64 on linux target for #242 2026-01-31 18:16:39 +08:00
Fazri Gading
8719cd3138 fix: revert to previous release.yml (#238) 2026-01-31 00:09:22 +01:00
Fazri Gading
611d436085 fix: change ownership back to the runner user (#237) 2026-01-30 23:55:17 +01:00
Fazri Gading
d5cc0868e9 Release Build v2.2.0 (#236)
* fix: resolve cross-platform EPERM permissions errors

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

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

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

* fix: missing pacman builds

* prepare release for 2.1.1

minor fix for EPERM error permission

* prepare release 2.1.1

minor fix EPERM permission error

* prepare release 2.1.1

* Update README.md Windows Prequisites for ARM64 builds

* fix: remove broken symlink after detected

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

* fix: isbrokenlink should be true to remove the symlink

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

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

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

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

* aur: add proper VCS (-git) PKGBUILD

created clean VCS-based PKGBUILD following arch packaging conventions.

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

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

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

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

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

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

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

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

* Update README.md

adds information for Arch build

* Update README.md

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

* Update PKGBUILD

* Update PKGBUILD-git

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

* Polish language support (#195)

* Update support_request.yml

Added hardware specification

* Update bug_report.yml

Add logs textfield to bug report

* chore: add changelog in README.md

* fix screenshot input in feature_request.yml

* add hardware spec input in bug_report.yml

* fix: PKGBUILD pkgname variable fix

* userdata migration [need review from other OS]

* french translate

* Add German and Swedish translations

Added de.json and sv.json locale files for German and Swedish language support. Updated i18n.js to register 'de' and 'sv' as available languages in the launcher.

* Update README.md

* chore: add offline-mode warning to the README.md

* chore: add downloads counter in README.md

* fix: Steam Deck/Ubuntu crash - use system libzstd.so

The bundled libzstd.so is incompatible with glibc 2.41's stricter heap
validation, causing "free(): invalid pointer" crashes.

Solution: Automatically replace bundled libzstd.so with system version
on Linux. The launcher detects and symlinks to /usr/lib/libzstd.so.1.

- Auto-detect system libzstd at common paths (Arch, Debian, Fedora)
- Backup bundled version as libzstd.so.bundled
- Create symlink to system version
- Add HYTALE_NO_LIBZSTD_FIX=1 to disable if needed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: remove Windows and Linux ARM64 information on the README.md

* Update support_request.yml

* fix: improve update system UX and macOS compatibility

Update System Improvements:
- Fix duplicate update popups by disabling legacy updater.js
- Add skip button to update popup (shows after 30s, on error, or after download)
- Add macOS-specific handling with manual download as primary option
- Add missing open-download-page IPC handler
- Add missing unblockInterface() method to properly clean up after popup close
- Add quitAndInstallUpdate alias in preload for compatibility
- Remove pulse animation when download completes
- Fix manual download button to show correct status and close popup
- Sync player name to settings input after first install

Client Patcher Cleanup:
- Remove server patching code (server uses pre-patched JAR from CDN)
- Simplify to client-only patching
- Remove unused imports (crypto, AdmZip, execSync, spawn, javaManager)
- Remove unused methods (stringToUtf8, findAndReplaceDomainUtf8)
- Move localhost dev code to backup file for reference

Code Quality Fixes:
- Fix duplicate DOMContentLoaded handlers in install.js
- Fix duplicate checkForUpdates definition in preload.js
- Fix redundant if/else in onProgressUpdate callback
- Fix typo "Harwadre" -> "Hardware" in preload.js

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add Russian language support

Added Russian (ru) to the list of available languages.

* chore: drafting documentation on SERVER.md

* Some updates in Russian language localization file

* fix

* Update ru.json

* Fixed Java runtime name and fixed typo

* fixed untranslated place

* Update ru.json

* Update ru.json

* Update ru.json

* Update ru.json

* Update ru.json

* fix: timeout getLatestClient 

fixes #138

* fix: change default version to 7.pwr in main.js

* fix: change default release version to 7.pwr

* fix: change version release to 7.pwr

* docs: Add comprehensive troubleshooting guide (#209)

Add TROUBLESHOOTING.md with solutions for common issues including:

- Windows: Firewall configuration, duplicate mods, SmartScreen
- Linux: GPU detection (NVIDIA/AMD), SDL3_image/libpng dependencies,
  Wayland/X11 issues, Steam Deck support
- macOS: Rosetta 2 for Apple Silicon, code signing, quarantine
- Connection: Server boot failures, regional restrictions
- Authentication: Token errors, config reset procedures
- Avatar/Cosmetics: F2P limitations documentation
- Backup locations for all platforms
- Log locations for bug reports

Solutions compiled from closed GitHub issues (#205, #155, #90, #60,
#144, #192) and community feedback.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* Standardize language codes, improve formatting, and update all locale files. (#224)

* Update German (Germany) localization

* Update Español (España) localization

* Update French (France) localization

* Update Polish (Poland) localization

* Update Portuguese (Brazil) localization

* Update Russian (Russia) localization

* Update Swedish (Sweden) localization

* Update Turkish (Turkey) localization

* Update language codes, names and alphabetical in i18n system

* Changed Spanish language name to the Formal name "Spanish (Spain)"

* Fix PKGBUILD-git

* Fix PKGBUILD

* delete cache after installation

* Enforce 16-char player name limit and update mod sync

Added a maxlength attribute to the player name input and enforced a 16-character limit in both install and settings scripts, providing user feedback if exceeded. Refactored modManager.js to replace symlink-based mod management with a copy-based system, copying enabled mods to HytaleSaves\Mods and removing legacy symlink logic to improve compatibility and avoid permission issues.

* Update installation subtitle

* chore: update quickstart link in README.md

* chore: delete warning of Ubuntu-Debian at Linux Prequisites section

* added featured server list from api

* Add Featured Servers page to GUI

* Update Discord invite URL in client patcher

* Add differential update system

* Remove launcher chat and add Discord popup

* fix: removed 'check disk space' alert on permission file error

* fix: upgrade tar to ^7.5.6 version

* fix: re-add universal arch for mac

* fix: upgrade electron/rebuild to 4.0.3

* fix: removed override tar version

* fix: pkgbuild version to 2.1.2

* fix: src.tar.zst and srcinfo missing files

* feat: add Indonesian language translation

* fix: GPU preference hint to Laptop-only

* feat: create two columns for settings page

* Add Discord invite link to rpc

* docs: add recordings form, fix OS list

* Release v2.2.0

* Release v2.2.0

* Release v2.2.0

* chore: delete icon.ico, moved to build folder

* chore: delete icon.png, moved to build folder

* fix: build and release for tag push-only in release.yml

* fix: gamescope steam deck issue fixes #186 hopefully

* Support branch selection for server patching

* chose: add auto-patch system for pre-release JAR

---------

Co-authored-by: TalesAmaral <57869141+TalesAmaral@users.noreply.github.com>
Co-authored-by: walti0 <95646872+walti0@users.noreply.github.com>
Co-authored-by: AMIAY <letudiantenrap.collab@gmail.com>
Co-authored-by: sanasol <mail@sanasol.ws>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Terromur <79866197+Terromur@users.noreply.github.com>
Co-authored-by: Zakhar Smokotov <zaharb840@gmail.com>
Co-authored-by: xSamiVS <samtaiebc@gmail.com>
2026-01-30 23:19:46 +01:00
Fazri Gading
a21e7e4910 chose: add auto-patch system for pre-release JAR 2026-01-31 05:52:20 +08:00
AMIAY
14a63febc1 Support branch selection for server patching 2026-01-30 22:45:21 +01:00
Fazri Gading
2cdef44fec fix: gamescope steam deck issue fixes #186 hopefully 2026-01-31 05:26:43 +08:00
Fazri Gading
f8cf41972d fix: build and release for tag push-only in release.yml 2026-01-31 04:34:58 +08:00
Fazri Gading
ea0f87c46a chore: delete icon.png, moved to build folder 2026-01-31 04:23:55 +08:00
Fazri Gading
a5b3fe02c8 chore: delete icon.ico, moved to build folder 2026-01-31 04:23:33 +08:00
Fazri Gading
0bb82a0b3d Release v2.2.0 2026-01-31 04:22:05 +08:00
Fazri Gading
eccdcf223e Release v2.2.0 2026-01-31 04:15:50 +08:00
Fazri Gading
a09b082152 Release v2.2.0 2026-01-31 04:04:34 +08:00
Fazri Gading
f1d01ac78c docs: add recordings form, fix OS list 2026-01-31 02:56:37 +08:00
AMIAY
bfe0156606 Add Discord invite link to rpc 2026-01-30 19:02:12 +01:00
Fazri Gading
78e97bdbb7 feat: create two columns for settings page 2026-01-31 01:42:20 +08:00
Fazri Gading
769bc2054c fix: GPU preference hint to Laptop-only 2026-01-31 00:52:02 +08:00
Fazri Gading
5337441d97 feat: add Indonesian language translation 2026-01-31 00:51:24 +08:00
Fazri Gading
12453d2dda fix: src.tar.zst and srcinfo missing files 2026-01-30 23:50:54 +08:00
Fazri Gading
803df90fb6 fix: pkgbuild version to 2.1.2 2026-01-30 23:50:29 +08:00
Fazri Gading
6c31c39abd fix: removed override tar version 2026-01-30 23:23:13 +08:00
Fazri Gading
b5ab8b78e8 fix: upgrade electron/rebuild to 4.0.3 2026-01-30 22:50:14 +08:00
Fazri Gading
343f7b8016 Merge branch 'develop' 2026-01-30 22:39:43 +08:00
Fazri Gading
fa568fcce7 fix: re-add universal arch for mac 2026-01-30 22:39:26 +08:00
Fazri Gading
a6ecd2c167 Merge branch 'develop' 2026-01-30 22:26:50 +08:00
Fazri Gading
3e1c4aef73 fix: upgrade tar to ^7.5.6 version 2026-01-30 22:24:56 +08:00
Fazri Gading
1c14c3f603 fix: removed 'check disk space' alert on permission file error 2026-01-30 22:13:01 +08:00
AMIAY
30a4327655 Remove launcher chat and add Discord popup 2026-01-30 14:44:46 +01:00
AMIAY
33a0e219fc Add differential update system 2026-01-30 04:11:10 +01:00
AMIAY
fbdd9ee0cf Update Discord invite URL in client patcher 2026-01-30 02:28:45 +01:00
sanasol
dfe9ed2a89 ci: set 6-hour max timeout for macOS notarization
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 20:59:28 +01:00
AMIAY
22ea2f56d3 Add Featured Servers page to GUI 2026-01-29 19:00:13 +01:00
AMIAY
5039bcdadf added featured server list from api 2026-01-29 17:07:29 +01:00
Fazri Gading
4db8016a28 chore: delete warning of Ubuntu-Debian at Linux Prequisites section 2026-01-29 23:15:54 +08:00
Fazri Gading
e0fd7e6900 chore: update quickstart link in README.md 2026-01-29 23:14:22 +08:00
AMIAY
93a2a98028 Update installation subtitle 2026-01-29 03:38:46 +01:00
AMIAY
4775e9adbd Enforce 16-char player name limit and update mod sync
Added a maxlength attribute to the player name input and enforced a 16-character limit in both install and settings scripts, providing user feedback if exceeded. Refactored modManager.js to replace symlink-based mod management with a copy-based system, copying enabled mods to HytaleSaves\Mods and removing legacy symlink logic to improve compatibility and avoid permission issues.
2026-01-29 03:33:56 +01:00
AMIAY
90db069e4c delete cache after installation 2026-01-29 00:58:47 +01:00
Terromur
baa585d6b3 Fix PKGBUILD 2026-01-29 04:49:02 +05:00
Terromur
a5b930a9f0 Fix PKGBUILD-git 2026-01-29 04:45:44 +05:00
xSamiVS
b708f4a7d7 Standardize language codes, improve formatting, and update all locale files. (#224)
* Update German (Germany) localization

* Update Español (España) localization

* Update French (France) localization

* Update Polish (Poland) localization

* Update Portuguese (Brazil) localization

* Update Russian (Russia) localization

* Update Swedish (Sweden) localization

* Update Turkish (Turkey) localization

* Update language codes, names and alphabetical in i18n system

* Changed Spanish language name to the Formal name "Spanish (Spain)"
2026-01-29 03:25:47 +08:00
Alex
28a4f65f21 docs: Add comprehensive troubleshooting guide (#209)
Add TROUBLESHOOTING.md with solutions for common issues including:

- Windows: Firewall configuration, duplicate mods, SmartScreen
- Linux: GPU detection (NVIDIA/AMD), SDL3_image/libpng dependencies,
  Wayland/X11 issues, Steam Deck support
- macOS: Rosetta 2 for Apple Silicon, code signing, quarantine
- Connection: Server boot failures, regional restrictions
- Authentication: Token errors, config reset procedures
- Avatar/Cosmetics: F2P limitations documentation
- Backup locations for all platforms
- Log locations for bug reports

Solutions compiled from closed GitHub issues (#205, #155, #90, #60,
#144, #192) and community feedback.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 03:24:39 +08:00
Fazri Gading
966de83ead fix: change version release to 7.pwr 2026-01-29 03:23:19 +08:00
Fazri Gading
bc7f46cf45 fix: change default release version to 7.pwr 2026-01-29 03:22:30 +08:00
Fazri Gading
534b3f1f34 fix: change default version to 7.pwr in main.js 2026-01-29 03:19:17 +08:00
Fazri Gading
a07f0f1de1 fix: timeout getLatestClient
fixes #138
2026-01-29 03:01:38 +08:00
Terromur
bf29112848 Merge pull request #218 from BlackSystemCoder/develop
Add Russian language support
2026-01-28 22:01:35 +05:00
Zakhar Smokotov
0e4e332dab Update ru.json 2026-01-28 19:53:46 +03:00
Zakhar Smokotov
779f6820cb Update ru.json 2026-01-28 19:49:37 +03:00
Zakhar Smokotov
4fc4d77415 Update ru.json 2026-01-28 19:47:52 +03:00
Zakhar Smokotov
de193e991f Update ru.json 2026-01-28 19:46:30 +03:00
Zakhar Smokotov
d69695e499 Update ru.json 2026-01-28 19:45:29 +03:00
Zakhar Smokotov
4fff87f221 fixed untranslated place 2026-01-28 19:40:39 +03:00
Zakhar Smokotov
4cd76bb96d Fixed Java runtime name and fixed typo 2026-01-28 19:39:41 +03:00
Zakhar Smokotov
721d287036 Update ru.json 2026-01-28 19:33:36 +03:00
Zakhar Smokotov
e491bf1a84 fix 2026-01-28 19:17:37 +03:00
Zakhar Smokotov
89f981b586 Some updates in Russian language localization file 2026-01-28 19:16:19 +03:00
Fazri Gading
9cf504bbcc chore: drafting documentation on SERVER.md 2026-01-28 23:41:27 +08:00
sanasol
0aaf74a3db fix: add verbose logging to notarize script for debugging
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 15:14:59 +01:00
sanasol
be78f67439 chore: update package-lock.json
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 15:04:13 +01:00
sanasol
d0b9ae1da8 ci: separate macOS release from main release job
macOS notarization is slow (5-10 min). Now release is created
immediately when Windows/Linux/Arch complete, and macOS uploads
to the same release when notarization finishes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 15:01:57 +01:00
sanasol
e8105cb30e feat: add macOS code signing and notarization support
- Add entitlements.mac.plist for hardened runtime
- Add notarize.js post-sign hook for Apple notarization
- Update package.json with signing config and @electron/notarize dep
- Update GitHub Actions workflow with signing secrets

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 14:48:40 +01:00
Zakhar Smokotov
e7110936d8 Add Russian language support
Added Russian (ru) to the list of available languages.
2026-01-28 16:27:48 +03:00
AMIAY
79456e43a6 Merge pull request #213 from amiayweb/fix/update-system-improvements 2026-01-28 03:14:05 +01:00
sanasol
dd2dbc6f08 fix: improve update system UX and macOS compatibility
Update System Improvements:
- Fix duplicate update popups by disabling legacy updater.js
- Add skip button to update popup (shows after 30s, on error, or after download)
- Add macOS-specific handling with manual download as primary option
- Add missing open-download-page IPC handler
- Add missing unblockInterface() method to properly clean up after popup close
- Add quitAndInstallUpdate alias in preload for compatibility
- Remove pulse animation when download completes
- Fix manual download button to show correct status and close popup
- Sync player name to settings input after first install

Client Patcher Cleanup:
- Remove server patching code (server uses pre-patched JAR from CDN)
- Simplify to client-only patching
- Remove unused imports (crypto, AdmZip, execSync, spawn, javaManager)
- Remove unused methods (stringToUtf8, findAndReplaceDomainUtf8)
- Move localhost dev code to backup file for reference

Code Quality Fixes:
- Fix duplicate DOMContentLoaded handlers in install.js
- Fix duplicate checkForUpdates definition in preload.js
- Fix redundant if/else in onProgressUpdate callback
- Fix typo "Harwadre" -> "Hardware" in preload.js

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 01:48:58 +01:00
Fazri Gading
c4acb32fcd Update support_request.yml 2026-01-28 05:16:00 +08:00
Fazri Gading
fbcbafb9b5 chore: remove Windows and Linux ARM64 information on the README.md 2026-01-28 04:26:42 +08:00
Terromur
86ed33358c Merge pull request #210 from amiayweb/fix/steamdeck-libzstd
fix: Steam Deck/Ubuntu crash - use system libzstd.so
2026-01-27 23:51:04 +05:00
sanasol
9ec97f9d33 fix: Steam Deck/Ubuntu crash - use system libzstd.so
The bundled libzstd.so is incompatible with glibc 2.41's stricter heap
validation, causing "free(): invalid pointer" crashes.

Solution: Automatically replace bundled libzstd.so with system version
on Linux. The launcher detects and symlinks to /usr/lib/libzstd.so.1.

- Auto-detect system libzstd at common paths (Arch, Debian, Fedora)
- Backup bundled version as libzstd.so.bundled
- Create symlink to system version
- Add HYTALE_NO_LIBZSTD_FIX=1 to disable if needed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 19:40:42 +01:00
Fazri Gading
ee18455b4b chore: add downloads counter in README.md 2026-01-27 21:14:53 +08:00
Fazri Gading
a5c931b26d chore: add offline-mode warning to the README.md 2026-01-27 18:45:55 +08:00
Fazri Gading
661a0c9eed Update README.md 2026-01-27 17:38:33 +08:00
Fazri Gading
6bd63f5b60 Merge branch 'amiayweb:main' into main 2026-01-27 04:14:19 +08:00
Fazri Gading
da186333cb Release Stable Build v2.1.1 (#198)
* fix: resolve cross-platform EPERM permissions errors

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

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

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

* fix: missing pacman builds

* prepare release for 2.1.1

minor fix for EPERM error permission

* prepare release 2.1.1

minor fix EPERM permission error

* prepare release 2.1.1

* Update README.md Windows Prequisites for ARM64 builds

* fix: remove broken symlink after detected

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

* fix: isbrokenlink should be true to remove the symlink

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

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

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

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

* aur: add proper VCS (-git) PKGBUILD

created clean VCS-based PKGBUILD following arch packaging conventions.

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

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

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

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

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

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

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

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

* Update README.md

adds information for Arch build

* Update README.md

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

* Update PKGBUILD

* Update PKGBUILD-git

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

* Polish language support (#195)

* Update support_request.yml

Added hardware specification

* Update bug_report.yml

Add logs textfield to bug report

* chore: add changelog in README.md

* fix screenshot input in feature_request.yml

* add hardware spec input in bug_report.yml

* fix: PKGBUILD pkgname variable fix

---------

Co-authored-by: TalesAmaral <57869141+TalesAmaral@users.noreply.github.com>
Co-authored-by: walti0 <95646872+walti0@users.noreply.github.com>
2026-01-27 04:11:10 +08:00
Fazri Gading
663ac5f834 Merge branch 'develop' fix PKGBUILD pkgname variable 2026-01-27 03:58:14 +08:00
Fazri Gading
ae375f9b6e Release Stable Build v2.1.1 (#197)
* fix: resolve cross-platform EPERM permissions errors

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

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

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

* fix: missing pacman builds

* Update README.md Windows Prequisites for ARM64 builds

* fix: remove broken symlink after detected

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

* fix: isbrokenlink should be true to remove the symlink

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

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

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

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

* aur: add proper VCS (-git) PKGBUILD

created clean VCS-based PKGBUILD following arch packaging conventions.

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

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

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

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

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

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

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

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

* Update README.md

adds information for Arch build

* Update README.md

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

* Update PKGBUILD

* Update PKGBUILD-git

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

* Polish language support (#195)

* add hardware specification in support_request.yml

* Add logs text field in bug_report.yml

* chore: add changelog in README.md

* fix screenshot input in feature_request.yml

* add hardware spec input in bug_report.yml

---------

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

View File

@@ -41,17 +41,17 @@ body:
required: true
- type: textarea
id: screenshots
id: proof
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
label: Screenshots/Recordings
description: If applicable, add Screenshots/Recordings to help explain your problem.
- type: input
id: version
attributes:
label: Version
description: What version of the launcher are you running?
placeholder: "e.g. \"v2.0.11 stable/pre-release\""
placeholder: "e.g. \"v2.2.0 stable\""
validations:
required: true
@@ -60,7 +60,7 @@ body:
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"
placeholder: "CPU: Intel i9-14900K 6.0 GHz | GPU: NVIDIA RTX 4090 24 GB VRAM | RAM: 32 GB"
validations:
required: true
@@ -70,13 +70,11 @@ body:
label: Operating System
description: What operating system are you using?
options:
- Windows 10
- Windows 11
- macOS (Apple Silicon)
- macOS (Intel)
- Linux Ubuntu/Debian-based
- Linux Fedora/RHEL-based
- Linux Arch-based
- Windows 11/10
- macOS (Apple Silicon, M1/M2/M3)
- Linux Ubuntu/Debian-based (Linux Mint, Pop!_OS, etc.)
- Linux Fedora/RHEL-based (Fedora, CentOS, etc.)
- Linux Arch-based (Steamdeck, CachyOS, etc.)
validations:
required: true

View File

@@ -1,8 +1,22 @@
name: Support Request
description: Request help or support
title: "[SUPPORT] "
title: "[SUPPORT] <ADD YOUR TITLE HERE>"
labels: ["support"]
body:
- type: dropdown
id: acknowledge
attributes:
label: Checklist
options:
- label: I have read the README.md before asking Support Request.
required: true
- label: I have read the TROUBLESHOOTING.md before asking Support Request.
required: true
- label: I have added title before submitting this Support Request.
required: true
- label: I acknowledge that my Support Request will not be responded as quick as in Discord Open-A-Ticket, I prefer this way.
required: true
- type: markdown
attributes:
value: |
@@ -24,10 +38,16 @@ body:
attributes:
label: Context
description: Provide any relevant context or background information.
placeholder: "I've tried..., but got..."
placeholder: "I've tried these steps, but got..."
validations:
required: true
- type: textarea
id: proof
attributes:
label: Screenshots/Recordings
description: If applicable, add Screenshots/Recordings to help explain your problem.
- type: textarea
id: hardwarespec
attributes:
@@ -37,12 +57,17 @@ body:
validations:
required: true
- type: input
- type: dropdown
id: version
attributes:
label: Version
description: What version are you using?
placeholder: "e.g. v2.0.11 stable/pre-release"
description: What launcher version are you using?
options:
- v2.2.0
- v2.1.1
- v2.1.0
- v2.0.11
- v2.0.2
validations:
required: true
@@ -52,13 +77,11 @@ body:
label: Platform
description: What platform are you using?
options:
- Windows 10
- Windows 11
- macOS (Apple Silicon)
- macOS (Intel)
- Linux Ubuntu/Debian-based
- Linux Fedora/RHEL-based
- Linux Arch-based
- Windows 11/10
- macOS (Apple Silicon, M1/M2/M3)
- Linux Ubuntu/Debian-based (Linux Mint, Pop!_OS, etc.)
- Linux Fedora/RHEL-based (Fedora, CentOS, etc.)
- Linux Arch-based (Steamdeck, CachyOS, etc.)
validations:
required: true

View File

@@ -2,8 +2,6 @@ name: Build and Release
on:
push:
branches:
- main
tags:
- 'v*'
workflow_dispatch:
@@ -29,8 +27,10 @@ jobs:
dist/*.exe.blockmap
dist/latest.yml
# macOS Universal build (ARM64 + x64 in single binary)
build-macos:
runs-on: macos-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -39,14 +39,27 @@ jobs:
cache: 'npm'
- run: npm ci
- name: Build macOS Packages
run: npx electron-builder --mac --publish never
- name: Build macOS Universal Package
env:
# Code signing
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
# Notarization (built-in electron-builder)
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: npx electron-builder --mac --universal --publish never
- name: List built artifacts
run: ls -la dist/
- uses: actions/upload-artifact@v4
with:
name: macos-builds
path: |
dist/*.dmg
dist/*.zip
dist/*.blockmap
dist/latest-mac.yml
build-linux:
@@ -66,7 +79,7 @@ jobs:
- name: Build Linux Packages
run: |
npx electron-builder --linux AppImage deb rpm --x64 --arm64 --publish never
npx electron-builder --linux AppImage deb rpm --publish never
- uses: actions/upload-artifact@v4
with:
name: linux-builds
@@ -108,11 +121,16 @@ jobs:
- name: Build Arch Package
run: |
sudo -u builder bash << 'EOF'
sudo -u builder bash << 'EOFBUILD'
set -e
makepkg --printsrcinfo > .SRCINFO
makepkg -s --noconfirm
EOF
EOFBUILD
- name: Fix permissions for upload
if: always()
run: |
sudo chown -R $(id -u):$(id -g) .
- name: Upload Arch Package
uses: actions/upload-artifact@v4
@@ -120,11 +138,12 @@ jobs:
name: arch-package
path: |
*.pkg.tar.zst
*.src.tar.zst
.SRCINFO
include-hidden-files: true
# Create release with all builds
release:
needs: [build-windows, build-macos, build-linux, build-arch]
needs: [build-windows, build-linux, build-arch, build-macos]
runs-on: ubuntu-latest
if: |
startsWith(github.ref, 'refs/tags/v') ||
@@ -135,14 +154,32 @@ jobs:
contents: write
steps:
# FIX: './package.json' Module Not Found in `Get version` step
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
- name: Download Windows artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
name: windows-builds
path: artifacts/windows-builds
- name: Download Linux artifacts
uses: actions/download-artifact@v4
with:
name: linux-builds
path: artifacts/linux-builds
- name: Download Arch artifacts
uses: actions/download-artifact@v4
with:
name: arch-package
path: artifacts/arch-package
- name: Download macOS artifacts
uses: actions/download-artifact@v4
with:
name: macos-builds
path: artifacts/macos-builds
- name: Display structure of downloaded files
run: ls -R artifacts
@@ -155,18 +192,13 @@ jobs:
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) }}
# 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/**/*
artifacts/linux-builds/*
artifacts/windows-builds/*
artifacts/macos-builds/*
generate_release_notes: true
draft: true
prerelease: false

View File

@@ -35,6 +35,10 @@
<i class="fas fa-play"></i>
<span class="nav-tooltip" data-i18n="nav.play">Play</span>
</div>
<div class="nav-item" data-page="featured">
<i class="fas fa-server"></i>
<span class="nav-tooltip">Featured Servers</span>
</div>
<div class="nav-item" data-page="mods">
<i class="fas fa-box"></i>
<span class="nav-tooltip" data-i18n="nav.mods">Mods</span>
@@ -43,10 +47,6 @@
<i class="fas fa-newspaper"></i>
<span class="nav-tooltip" data-i18n="nav.news">News</span>
</div>
<div class="nav-item" data-page="chat">
<i class="fas fa-comments"></i>
<span class="nav-tooltip" data-i18n="nav.chat">Players Chat</span>
</div>
<div class="nav-item" data-page="settings">
<i class="fas fa-cog"></i>
<span class="nav-tooltip" data-i18n="nav.settings">Settings</span>
@@ -55,6 +55,10 @@
<i class="fas fa-terminal"></i>
<span class="nav-tooltip">Logs</span>
</div>
<div class="nav-item" onclick="openDiscordExternal()">
<i class="fab fa-discord"></i>
<span class="nav-tooltip">Discord</span>
</div>
</div>
@@ -112,7 +116,7 @@
<h1 class="install-title">
HY<span class="title-accent">TALE</span>
</h1>
<p class="install-subtitle" data-i18n="install.title">FREE TO PLAY LAUNCHER</p>
<p class="install-subtitle" data-i18n="install.title">UNOFFICIAL HYTALE LAUNCHER</p>
</div>
<div class="install-form">
@@ -120,7 +124,7 @@
<label class="form-label" data-i18n="install.playerName">Player Name</label>
<input type="text" id="installPlayerName"
data-i18n-placeholder="install.playerNamePlaceholder" class="form-input"
value="Player" />
value="Player" maxlength="16" />
</div>
<div class="form-group">
@@ -212,6 +216,38 @@
</div>
</div>
<div id="featured-page" class="page">
<div class="featured-layout">
<div class="featured-left">
<div class="featured-header">
<h2 class="featured-title">
<i class="fas fa-star mr-2"></i>
<span>FEATURED SERVERS</span>
</h2>
</div>
<div id="featuredServersList" class="featured-list">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin fa-2x"></i>
</div>
</div>
</div>
<div class="featured-right">
<div class="featured-header">
<h2 class="featured-title">
<i class="fas fa-server mr-2"></i>
<span>HF2P SERVERS</span>
</h2>
</div>
<div id="myServersList" class="featured-list">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div id="mods-page" class="page">
<div class="mods-header">
<div class="mods-search-container">
@@ -256,50 +292,6 @@
<div id="allNewsGrid" class="news-grid-full"></div>
</div>
<div id="chat-page" class="page">
<div class="chat-container">
<div class="chat-header">
<h2 class="chat-title">
<i class="fas fa-comments mr-2"></i>
<span data-i18n="chat.title">PLAYERS CHAT</span>
</h2>
<div class="chat-header-actions">
<button id="chatColorBtn" class="chat-color-btn">
<i class="fas fa-palette"></i>
<span data-i18n="chat.pickColor">Color</span>
</button>
<div class="chat-online-badge">
<i class="fas fa-circle"></i>
<span id="chatOnlineCount">0</span> <span data-i18n="chat.online">online</span>
</div>
</div>
</div>
<div class="chat-body">
<div id="chatMessages" class="chat-messages">
</div>
</div>
<div class="chat-footer">
<div class="chat-input-container">
<textarea id="chatInput" class="chat-input"
data-i18n-placeholder="chat.inputPlaceholder" rows="1"
maxlength="500"></textarea>
<button id="chatSendBtn" class="chat-send-btn">
<i class="fas fa-paper-plane"></i>
</button>
</div>
<div class="chat-footer-info">
<span class="chat-char-counter" id="chatCharCounter">0/500</span>
<span class="chat-warning-text">
<i class="fas fa-shield-alt"></i>
<span data-i18n="chat.secureChat">Secure chat - Links are censored</span>
</span>
</div>
</div>
</div>
</div>
<div id="settings-page" class="page">
<div class="settings-container">
<div class="settings-header">
@@ -310,272 +302,274 @@
</div>
<div class="settings-content">
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-gamepad"></i>
<span data-i18n="settings.game">Game Options</span>
</h3>
<div class="settings-column">
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-gamepad"></i>
<span data-i18n="settings.game">Game Options</span>
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.playerName">Player
Name</label>
<input type="text" id="settingsPlayerName" class="settings-input"
data-i18n-placeholder="settings.playerNamePlaceholder" maxlength="16" />
<p class="settings-hint">
<i class="fas fa-user"></i>
<span data-i18n="settings.playerNameHint">This name will be used in-game
(1-16 characters)</span>
</p>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.playerName">Player
Name</label>
<input type="text" id="settingsPlayerName" class="settings-input"
data-i18n-placeholder="settings.playerNamePlaceholder" maxlength="16" />
<p class="settings-hint">
<i class="fas fa-user"></i>
<span data-i18n="settings.playerNameHint">This name will be used in-game
(1-16 characters)</span>
</p>
</div>
</div>
</div>
<div class="settings-option">
<div class="settings-button-group">
<button id="openGameLocationBtn" class="settings-action-btn"
onclick="openGameLocation()">
<i class="fas fa-folder-open"></i>
<div class="btn-content">
<div class="btn-title" data-i18n="settings.openGameLocation">Open
Game Location</div>
<div class="btn-description"
data-i18n="settings.openGameLocationDesc">Open the game
installation folder</div>
</div>
</button>
</div>
</div>
<div class="settings-option">
<div class="settings-button-group">
<button id="repairGameBtn" class="settings-action-btn"
onclick="repairGame()">
<i class="fas fa-tools"></i>
<div class="btn-content">
<div class="btn-title" data-i18n="settings.repairGame">Repair Game
<div class="settings-option">
<div class="settings-button-group">
<button id="openGameLocationBtn" class="settings-action-btn"
onclick="openGameLocation()">
<i class="fas fa-folder-open"></i>
<div class="btn-content">
<div class="btn-title" data-i18n="settings.openGameLocation">Open
Game Location</div>
<div class="btn-description"
data-i18n="settings.openGameLocationDesc">Open the game
installation folder</div>
</div>
<div class="btn-description" data-i18n="settings.reinstallGame">
Reinstall game files (preserves data)
</div>
</div>
</button>
</button>
</div>
</div>
</div>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.gameBranch">Game
Branch</label>
<div class="settings-option">
<div class="settings-button-group">
<button id="repairGameBtn" class="settings-action-btn"
onclick="repairGame()">
<i class="fas fa-tools"></i>
<div class="btn-content">
<div class="btn-title" data-i18n="settings.repairGame">Repair Game
</div>
<div class="btn-description" data-i18n="settings.reinstallGame">
Reinstall game files (preserves data)
</div>
</div>
</button>
</div>
</div>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.gameBranch">Game
Branch</label>
<div class="segmented-control">
<input type="radio" id="branch-release" name="gameBranch"
value="release" checked>
<label for="branch-release"
data-i18n="settings.branchRelease">Release</label>
<input type="radio" id="branch-pre-release" name="gameBranch"
value="pre-release">
<label for="branch-pre-release"
data-i18n="settings.branchPreRelease">Pre-Release</label>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.branchHint">Switch between stable release and
experimental pre-release versions</span>
</p>
<p class="settings-hint" style="color: #f39c12;">
<i class="fas fa-exclamation-triangle"></i>
<span data-i18n="settings.branchWarning">Changing branch will download
and install a different game version</span>
</p>
</div>
</div>
<div class="settings-option">
<label class="settings-input-label" data-i18n="settings.gpuPreference">GPU
Preference</label>
<div class="segmented-control">
<input type="radio" id="branch-release" name="gameBranch"
value="release" checked>
<label for="branch-release"
data-i18n="settings.branchRelease">Release</label>
<input type="radio" id="branch-pre-release" name="gameBranch"
value="pre-release">
<label for="branch-pre-release"
data-i18n="settings.branchPreRelease">Pre-Release</label>
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto" checked>
<label for="gpu-auto" data-i18n="settings.gpuAuto">Auto</label>
<input type="radio" id="gpu-integrated" name="gpuPreference"
value="integrated">
<label for="gpu-integrated"
data-i18n="settings.gpuIntegrated">Integrated</label>
<input type="radio" id="gpu-dedicated" name="gpuPreference"
value="dedicated">
<label for="gpu-dedicated"
data-i18n="settings.gpuDedicated">Dedicated</label>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.branchHint">Switch between stable release and
experimental pre-release versions</span>
</p>
<p class="settings-hint" style="color: #f39c12;">
<i class="fas fa-exclamation-triangle"></i>
<span data-i18n="settings.branchWarning">Changing branch will download
and install a different game version</span>
<span data-i18n="settings.gpuHint">Laptop-only feature; set to Integrated if on PC</span>
</p>
<div id="gpu-detection-info" class="gpu-detection-info"></div>
</div>
</div>
<div class="settings-option">
<label class="settings-input-label" data-i18n="settings.gpuPreference">GPU
Preference</label>
<div class="segmented-control">
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto" checked>
<label for="gpu-auto" data-i18n="settings.gpuAuto">Auto</label>
<input type="radio" id="gpu-integrated" name="gpuPreference"
value="integrated">
<label for="gpu-integrated"
data-i18n="settings.gpuIntegrated">Integrated</label>
<input type="radio" id="gpu-dedicated" name="gpuPreference"
value="dedicated">
<label for="gpu-dedicated"
data-i18n="settings.gpuDedicated">Dedicated</label>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-coffee"></i>
<span data-i18n="settings.java">Java Runtime</span>
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="customJavaCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.useCustomJava">Use
Custom Java Path</div>
<div class="checkbox-description" data-i18n="settings.javaDescription">
Override the bundled Java runtime with
your own installation</div>
</div>
</label>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.gpuHint">Select your preferred GPU (Linux:
affects DRI_PRIME)</span>
</p>
<div id="gpu-detection-info" class="gpu-detection-info"></div>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-fingerprint"></i>
<span data-i18n="settings.account">Player UUID Management</span>
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.currentUUID">Current
UUID</label>
<div class="uuid-display-container">
<input type="text" id="currentUuid" class="settings-input uuid-input"
readonly data-i18n-placeholder="settings.uuidPlaceholder" />
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
<i class="fas fa-copy"></i>
</button>
<button id="regenerateUuidBtn" class="uuid-btn regenerate-btn"
title="Generate New UUID">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.uuidHint">Your unique player identifier for
this username</span>
</p>
</div>
</div>
<div class="settings-option">
<div class="settings-button-group">
<button id="manageUuidsBtn" class="settings-action-btn">
<i class="fas fa-list"></i>
<div class="btn-content">
<div class="btn-title" data-i18n="settings.manageUUIDs">Manage All
UUIDs</div>
<div class="btn-description" data-i18n="settings.manageUUIDsDesc">
View and manage all player UUIDs</div>
</div>
</button>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fab fa-discord"></i>
<span data-i18n="settings.discord">Discord Integration</span>
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="discordRPCCheck" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.enableRPC">Enable
Discord Rich Presence</div>
<div class="checkbox-description" data-i18n="settings.discordDescription">
Show your launcher activity
on Discord
<div id="customJavaOptions" class="custom-java-options" style="display: none;">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.javaPath">Java
Executable Path</label>
<div class="settings-input-with-button">
<input type="text" id="customJavaPath" class="settings-input"
data-i18n-placeholder="settings.javaPathPlaceholder" readonly />
<button id="browseJavaBtn" class="settings-browse-btn">
<i class="fas fa-folder-open"></i>
<span data-i18n="settings.javaBrowse">Browse</span>
</button>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.javaHint">Select the Java installation folder
(supports Windows, Mac, Linux)</span>
</p>
</div>
</div>
</label>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-window-close"></i>
<span data-i18n="settings.closeLauncher">Launcher Behavior</span>
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="closeLauncherCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.closeOnStart">Close Launcher
on game start</div>
<div class="checkbox-description"
data-i18n="settings.closeOnStartDescription">
Automatically close the launcher after Hytale has launched
</div>
</div>
</label>
</div>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="launcherHwAccelCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.hwAccel">Launcher Hardware
Acceleration</div>
<div class="checkbox-description" data-i18n="settings.hwAccelDescription">
Enable hardware acceleration for the launcher UI (Requires restart)
</div>
</div>
</label>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-coffee"></i>
<span data-i18n="settings.java">Java Runtime</span>
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="customJavaCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.useCustomJava">Use
Custom Java Path</div>
<div class="checkbox-description" data-i18n="settings.javaDescription">
Override the bundled Java runtime with
your own installation</div>
</div>
</label>
</div>
<div id="customJavaOptions" class="custom-java-options" style="display: none;">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.javaPath">Java
Executable Path</label>
<div class="settings-input-with-button">
<input type="text" id="customJavaPath" class="settings-input"
data-i18n-placeholder="settings.javaPathPlaceholder" readonly />
<button id="browseJavaBtn" class="settings-browse-btn">
<i class="fas fa-folder-open"></i>
<span data-i18n="settings.javaBrowse">Browse</span>
</button>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.javaHint">Select the Java installation folder
(supports Windows, Mac, Linux)</span>
</p>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-language"></i>
<span data-i18n="settings.language">Language</span>
</h3>
<div class="settings-column">
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-fingerprint"></i>
<span data-i18n="settings.account">Player UUID Management</span>
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.selectLanguage">Select
Language</label>
<select id="languageSelect" class="settings-input">
<!-- Options populated by i18n.js -->
</select>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.currentUUID">Current
UUID</label>
<div class="uuid-display-container">
<input type="text" id="currentUuid" class="settings-input uuid-input"
readonly data-i18n-placeholder="settings.uuidPlaceholder" />
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
<i class="fas fa-copy"></i>
</button>
<button id="regenerateUuidBtn" class="uuid-btn regenerate-btn"
title="Generate New UUID">
<i class="fas fa-sync-alt"></i>
</button>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.uuidHint">Your unique player identifier for
this username</span>
</p>
</div>
</div>
<div class="settings-option">
<div class="settings-button-group">
<button id="manageUuidsBtn" class="settings-action-btn">
<i class="fas fa-list"></i>
<div class="btn-content">
<div class="btn-title" data-i18n="settings.manageUUIDs">Manage All
UUIDs</div>
<div class="btn-description" data-i18n="settings.manageUUIDsDesc">
View and manage all player UUIDs</div>
</div>
</button>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-window-close"></i>
<span data-i18n="settings.closeLauncher">Launcher Behavior</span>
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="closeLauncherCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.closeOnStart">Close Launcher
on game start</div>
<div class="checkbox-description"
data-i18n="settings.closeOnStartDescription">
Automatically close the launcher after Hytale has launched
</div>
</div>
</label>
</div>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="launcherHwAccelCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.hwAccel">Launcher Hardware
Acceleration</div>
<div class="checkbox-description" data-i18n="settings.hwAccelDescription">
Enable hardware acceleration for the launcher UI (Requires restart)
</div>
</div>
</label>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-language"></i>
<span data-i18n="settings.language">Language</span>
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.selectLanguage">Select
Language</label>
<select id="languageSelect" class="settings-input">
<!-- Options populated by i18n.js -->
</select>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fab fa-discord"></i>
<span data-i18n="settings.discord">Discord Integration</span>
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="discordRPCCheck" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title" data-i18n="settings.enableRPC">Enable
Discord Rich Presence</div>
<div class="checkbox-description" data-i18n="settings.discordDescription">
Show your launcher activity
on Discord
</div>
</div>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="logs-page" class="page">
<div class="logs-container">
@@ -659,41 +653,6 @@
</div>
</div>
<div id="chatUsernameModal" class="chat-username-modal" style="display: none;">
<div class="chat-username-modal-content">
<div class="chat-username-modal-header">
<h2 class="chat-username-modal-title">
<i class="fas fa-comments mr-2"></i>
<span data-i18n="chat.joinChat">Join Chat</span>
</h2>
</div>
<div class="chat-username-modal-body">
<p class="chat-username-modal-description" data-i18n="chat.chooseUsername">
Choose a username to join the Players Chat
</p>
<div class="chat-username-input-group">
<label for="chatUsernameInput" class="chat-username-label"
data-i18n="chat.username">Username</label>
<input type="text" id="chatUsernameInput" class="chat-username-input"
data-i18n-placeholder="chat.usernamePlaceholder" maxlength="20" autocomplete="off" />
<span class="chat-username-hint" data-i18n="chat.usernameHint">3-20 characters, letters, numbers, -
and _ only</span>
<span id="chatUsernameError" class="chat-username-error"></span>
</div>
</div>
<div class="chat-username-modal-footer">
<button id="chatUsernameCancel" class="chat-username-btn-cancel">
<i class="fas fa-times"></i>
<span data-i18n="common.cancel">Cancel</span>
</button>
<button id="chatUsernameSubmit" class="chat-username-btn-submit">
<i class="fas fa-check"></i>
<span data-i18n="chat.joinButton">Join Chat</span>
</button>
</div>
</div>
</div>
<!-- UUID Management Modal -->
<div id="uuidModal" class="uuid-modal" style="display: none;">
<div class="uuid-modal-content">
@@ -787,7 +746,6 @@
<div class="version-display-bottom">
<i class="fas fa-code-branch"></i>
<span id="launcherVersion">Loading...</span>
</div>
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
@@ -818,71 +776,47 @@
</div>
</footer>
<script type="module" src="js/script.js"></script> <!-- Discord Notification -->
<div id="discordNotification" class="discord-notification">
<div class="notification-content">
<i class="fab fa-discord"></i>
<span class="notification-text" data-i18n="discord.notificationText">Join our Discord community!</span>
<button class="notification-action"
onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
<span data-i18n="discord.joinButton">Join Discord</span>
</button>
</div>
<button class="notification-close" onclick="closeDiscordNotification()">
<i class="fas fa-times"></i>
</button>
</div>
<script type="module" src="js/script.js"></script>
<!-- Modal pour sélectionner la couleur du chat -->
<div id="chatColorModal" class="chat-color-modal" style="display: none;">
<div class="chat-color-modal-content">
<div class="chat-color-modal-header">
<h3 class="chat-color-modal-title">
<i class="fas fa-palette"></i>
<span data-i18n="chat.colorModal.title">Customize Username Color</span>
</h3>
<button class="modal-close-btn" onclick="closeChatColorModal()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="chat-color-modal-body">
<div id="solidColorSection" class="color-section">
<h4 data-i18n="chat.colorModal.chooseSolid">Choose a solid color:</h4>
<div class="predefined-colors">
<div class="color-option" data-color="#3498db" style="background: #3498db;"></div>
<div class="color-option" data-color="#e74c3c" style="background: #e74c3c;"></div>
<div class="color-option" data-color="#2ecc71" style="background: #2ecc71;"></div>
<div class="color-option" data-color="#f39c12" style="background: #f39c12;"></div>
<div class="color-option" data-color="#9b59b6" style="background: #9b59b6;"></div>
<div class="color-option" data-color="#1abc9c" style="background: #1abc9c;"></div>
<div class="color-option" data-color="#e91e63" style="background: #e91e63;"></div>
<div class="color-option" data-color="#ff5722" style="background: #ff5722;"></div>
</div>
<div class="custom-color-input">
<label for="customColor" data-i18n="chat.colorModal.customColor">Custom color:</label>
<input type="color" id="customColor" value="#3498db">
</div>
</div>
<div class="color-preview">
<h4 data-i18n="chat.colorModal.preview">Preview:</h4>
<div id="colorPreview" class="preview-username" data-i18n="chat.colorModal.previewUsername">
YourUsername</div>
<div id="discordPopupModal" class="modal-overlay" style="display: none;">
<div class="modal-content discord-popup-modal">
<div class="modal-header">
<div class="discord-popup-header">
<i class="fab fa-discord"></i>
<h2 class="modal-title">Join Our Discord Community</h2>
</div>
</div>
<div class="chat-color-modal-footer">
<button class="btn-secondary" onclick="closeChatColorModal()"><span
data-i18n="common.cancel">Cancel</span></button>
<button class="btn-primary" onclick="applyChatColor()"><span data-i18n="chat.colorModal.apply">Apply
Color</span></button>
<div class="modal-body">
<div class="discord-popup-body">
<p class="discord-popup-text">
Join our community of over <strong>5000 members</strong> and stay connected!
</p>
<p class="discord-popup-text">
Get the latest news, updates, and announcements about the launcher.
</p>
<p class="discord-popup-text">
Find help, report bugs, share your feedback, and connect with other players.
</p>
<div class="discord-popup-actions">
<button class="discord-popup-btn primary" onclick="joinDiscord()">
<i class="fab fa-discord"></i>
Join Discord
</button>
<button class="discord-popup-btn secondary" onclick="closeDiscordPopup()">
Maybe Later
</button>
</div>
</div>
</div>
</div>
</div>
<script src="js/i18n.js"></script>
<script src="js/featured.js"></script>
<script type="module" src="js/settings.js"></script>
<script type="module" src="js/update.js"></script>
<script src="js/updater.js"></script>
<!-- updater.js disabled - using update.js instead which has skip button and macOS handling -->
</body>

View File

@@ -1,500 +0,0 @@
let socket = null;
let isAuthenticated = false;
let messageQueue = [];
let chatUsername = '';
let userColor = '#3498db';
let userBadge = null;
const SOCKET_URL = 'https://chat.hytalef2p.com';
const MAX_MESSAGE_LENGTH = 500;
async function getOrCreatePlayerId() {
return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
export async function initChat() {
if (window.electronAPI?.loadChatUsername) {
chatUsername = await window.electronAPI.loadChatUsername();
}
if (window.electronAPI?.loadChatColor) {
const savedColor = await window.electronAPI.loadChatColor();
if (savedColor) {
userColor = savedColor;
}
}
if (!chatUsername || chatUsername.trim() === '') {
showUsernameModal();
return;
}
setupChatUI();
setupColorSelector();
await connectToChat();
}
function showUsernameModal() {
const modal = document.getElementById('chatUsernameModal');
if (modal) {
modal.style.display = 'flex';
const input = document.getElementById('chatUsernameInput');
if (input) {
setTimeout(() => input.focus(), 100);
}
}
}
function hideUsernameModal() {
const modal = document.getElementById('chatUsernameModal');
if (modal) {
modal.style.display = 'none';
}
}
async function submitChatUsername() {
const input = document.getElementById('chatUsernameInput');
const errorMsg = document.getElementById('chatUsernameError');
if (!input) return;
const username = input.value.trim();
if (username.length === 0) {
if (errorMsg) errorMsg.textContent = 'Username cannot be empty';
return;
}
if (username.length < 3) {
if (errorMsg) errorMsg.textContent = 'Username must be at least 3 characters';
return;
}
if (username.length > 20) {
if (errorMsg) errorMsg.textContent = 'Username must be 20 characters or less';
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
if (errorMsg) errorMsg.textContent = 'Username can only contain letters, numbers, - and _';
return;
}
chatUsername = username;
if (window.electronAPI?.saveChatUsername) {
await window.electronAPI.saveChatUsername(username);
}
hideUsernameModal();
setupChatUI();
await connectToChat();
}
function setupChatUI() {
const sendBtn = document.getElementById('chatSendBtn');
const chatInput = document.getElementById('chatInput');
const chatMessages = document.getElementById('chatMessages');
if (!sendBtn || !chatInput || !chatMessages) {
console.warn('Chat UI elements not found');
return;
}
sendBtn.addEventListener('click', () => {
sendMessage();
});
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
chatInput.addEventListener('input', () => {
if (chatInput.value.length > MAX_MESSAGE_LENGTH) {
chatInput.value = chatInput.value.substring(0, MAX_MESSAGE_LENGTH);
}
updateCharCounter();
});
updateCharCounter();
}
async function connectToChat() {
try {
if (!window.io) {
await loadSocketIO();
}
const userId = await window.electronAPI?.getUserId();
if (!userId) {
console.error('User ID not available');
addSystemMessage('Error: Could not connect to chat');
return;
}
if (!chatUsername || chatUsername.trim() === '') {
console.error('Chat username not set');
addSystemMessage('Error: Username not set');
return;
}
socket = io(SOCKET_URL, {
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000
});
socket.on('connect', async () => {
console.log('Connected to chat server');
const uuid = await window.electronAPI?.getCurrentUuid();
socket.emit('authenticate', {
username: chatUsername,
userId,
uuid: uuid,
userColor: userColor
});
});
socket.on('authenticated', (data) => {
isAuthenticated = true;
userBadge = data.badge;
addSystemMessage(`Connected as ${data.username}`);
while (messageQueue.length > 0) {
const msg = messageQueue.shift();
socket.emit('send_message', { message: msg });
}
});
socket.on('message', (data) => {
if (data.type === 'system') {
addSystemMessage(data.message);
} else if (data.type === 'user') {
addUserMessage(data.username, data.message, data.timestamp, data.userColor, data.badge);
}
});
socket.on('users_update', (data) => {
updateOnlineCount(data.count);
});
socket.on('error', (data) => {
addSystemMessage(`Error: ${data.message}`, 'error');
});
socket.on('clear_chat', (data) => {
clearAllMessages();
addSystemMessage(data.message || 'Chat cleared by server', 'warning');
});
socket.on('disconnect', () => {
isAuthenticated = false;
console.log('Disconnected from chat server');
addSystemMessage('Disconnected from chat', 'error');
});
socket.on('connect_error', (error) => {
console.error('Connection error:', error);
addSystemMessage('Connection error. Retrying...', 'error');
});
} catch (error) {
console.error('Error connecting to chat:', error);
addSystemMessage('Failed to connect to chat server', 'error');
}
}
function loadSocketIO() {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdn.socket.io/4.6.1/socket.io.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
function sendMessage() {
const chatInput = document.getElementById('chatInput');
const message = chatInput.value.trim();
if (!message || message.length === 0) {
return;
}
if (message.length > MAX_MESSAGE_LENGTH) {
addSystemMessage(`Message too long (max ${MAX_MESSAGE_LENGTH} characters)`, 'error');
return;
}
if (!socket || !isAuthenticated) {
messageQueue.push(message);
addSystemMessage('Connecting... Your message will be sent soon.', 'warning');
chatInput.value = '';
updateCharCounter();
return;
}
socket.emit('send_message', { message });
chatInput.value = '';
updateCharCounter();
}
function addUserMessage(username, message, timestamp, userColor = '#3498db', badge = null) {
const chatMessages = document.getElementById('chatMessages');
if (!chatMessages) return;
const messageDiv = document.createElement('div');
messageDiv.className = 'chat-message user-message';
const time = new Date(timestamp).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
let badgeHTML = '';
if (badge) {
let badgeStyle = '';
if (badge.style === 'rainbow') {
badgeStyle = `background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #ffeaa7, #fab1a0, #fd79a8); background-size: 400% 400%; animation: rainbow 3s ease infinite; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: bold; display: inline;`;
} else if (badge.style === 'gradient') {
if (badge.badge === 'CONTRIBUTOR') {
badgeStyle = `background: linear-gradient(45deg, #22c55e, #16a34a); background-size: 200% 200%; animation: contributorGlow 2s ease infinite; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: bold; display: inline;`;
} else {
badgeStyle = `color: ${badge.color}; font-weight: bold; display: inline;`;
}
}
badgeHTML = `<span class="user-badge" style="${badgeStyle}">[${badge.badge}]</span> `;
}
messageDiv.innerHTML = `
<div class="message-header">
<span class="message-user-info">${badgeHTML}<span class="message-username" style="font-weight: bold;" data-username-color="${userColor}">${escapeHtml(username)}</span></span>
<span class="message-time">${time}</span>
</div>
<div class="message-content">${message}</div>
`;
const usernameElement = messageDiv.querySelector('.message-username');
if (usernameElement) {
applyUserColorStyle(usernameElement, userColor);
}
chatMessages.appendChild(messageDiv);
scrollToBottom();
}
function addSystemMessage(message, type = 'info') {
const chatMessages = document.getElementById('chatMessages');
if (!chatMessages) return;
const messageDiv = document.createElement('div');
messageDiv.className = `chat-message system-message system-${type}`;
messageDiv.innerHTML = `
<div class="message-content">
<i class="fas fa-info-circle"></i> ${escapeHtml(message)}
</div>
`;
chatMessages.appendChild(messageDiv);
scrollToBottom();
}
function updateOnlineCount(count) {
const onlineCountElement = document.getElementById('chatOnlineCount');
if (onlineCountElement) {
onlineCountElement.textContent = count;
}
}
function updateCharCounter() {
const chatInput = document.getElementById('chatInput');
const charCounter = document.getElementById('chatCharCounter');
if (chatInput && charCounter) {
const length = chatInput.value.length;
charCounter.textContent = `${length}/${MAX_MESSAGE_LENGTH}`;
if (length > MAX_MESSAGE_LENGTH * 0.9) {
charCounter.classList.add('warning');
} else {
charCounter.classList.remove('warning');
}
}
}
function scrollToBottom() {
const chatMessages = document.getElementById('chatMessages');
if (chatMessages) {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
}
function clearAllMessages() {
const chatMessages = document.getElementById('chatMessages');
if (chatMessages) {
chatMessages.innerHTML = '';
console.log('Chat cleared');
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
window.addEventListener('beforeunload', () => {
if (socket && socket.connected) {
socket.disconnect();
}
});
document.addEventListener('DOMContentLoaded', () => {
const usernameSubmitBtn = document.getElementById('chatUsernameSubmit');
const usernameCancelBtn = document.getElementById('chatUsernameCancel');
const usernameInput = document.getElementById('chatUsernameInput');
if (usernameSubmitBtn) {
usernameSubmitBtn.addEventListener('click', submitChatUsername);
}
if (usernameCancelBtn) {
usernameCancelBtn.addEventListener('click', () => {
hideUsernameModal();
const playNavItem = document.querySelector('[data-page="play"]');
if (playNavItem) playNavItem.click();
});
}
if (usernameInput) {
usernameInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
submitChatUsername();
}
});
}
const chatNavItem = document.querySelector('[data-page="chat"]');
if (chatNavItem) {
chatNavItem.addEventListener('click', () => {
if (!socket) {
initChat();
}
});
}
});
function setupColorSelector() {
const colorBtn = document.getElementById('chatColorBtn');
if (colorBtn) {
colorBtn.addEventListener('click', showChatColorModal);
}
const colorOptions = document.querySelectorAll('.color-option');
colorOptions.forEach(option => {
option.addEventListener('click', () => {
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
option.classList.add('selected');
updateColorPreview();
});
});
const customColor = document.getElementById('customColor');
if (customColor) {
customColor.addEventListener('input', () => {
document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected'));
updateColorPreview();
});
}
}
function showChatColorModal() {
const modal = document.getElementById('chatColorModal');
if (modal) {
modal.style.display = 'flex';
updateColorPreview();
}
}
window.closeChatColorModal = function() {
const modal = document.getElementById('chatColorModal');
if (modal) {
modal.style.display = 'none';
}
}
function updateColorPreview() {
const preview = document.getElementById('colorPreview');
if (!preview) return;
const selectedOption = document.querySelector('.color-option.selected');
let color = '#3498db';
if (selectedOption) {
color = selectedOption.dataset.color;
} else {
const customColor = document.getElementById('customColor');
if (customColor) color = customColor.value;
}
preview.style.color = color;
preview.style.background = 'transparent';
preview.style.webkitBackgroundClip = 'initial';
preview.style.webkitTextFillColor = 'initial';
}
window.applyChatColor = async function() {
let newColor;
const selectedOption = document.querySelector('.color-option.selected');
if (selectedOption) {
newColor = selectedOption.dataset.color;
} else {
const customColor = document.getElementById('customColor');
newColor = customColor ? customColor.value : '#3498db';
}
userColor = newColor;
if (window.electronAPI?.saveChatColor) {
await window.electronAPI.saveChatColor(newColor);
}
if (socket && isAuthenticated) {
const uuid = await window.electronAPI?.getCurrentUuid();
socket.emit('authenticate', {
username: chatUsername,
userId: await getOrCreatePlayerId(),
uuid: uuid,
userColor: userColor
});
addSystemMessage('Username color updated successfully', 'success');
}
closeChatColorModal();
}
function applyUserColorStyle(element, color) {
element.style.color = color;
element.style.background = 'transparent';
element.style.webkitBackgroundClip = 'initial';
element.style.webkitTextFillColor = 'initial';
}
window.ChatAPI = {
send: sendMessage,
disconnect: () => socket?.disconnect()
};

176
GUI/js/featured.js Normal file
View File

@@ -0,0 +1,176 @@
// Featured Servers Management
const FEATURED_SERVERS_API = 'https://assets.authbp.xyz/featured.json';
/**
* Safely escape HTML while preserving UTF-8 characters
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Load and display featured servers
*/
async function loadFeaturedServers() {
const featuredContainer = document.getElementById('featuredServersList');
const myServersContainer = document.getElementById('myServersList');
try {
console.log('[FeaturedServers] Fetching from', FEATURED_SERVERS_API);
// Fetch featured servers from API (no cache)
const response = await fetch(FEATURED_SERVERS_API, {
cache: 'no-store',
headers: {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Accept-Charset': 'utf-8'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const text = await response.text();
const data = JSON.parse(text);
const featuredServers = data.featuredServers || [];
console.log('[FeaturedServers] Loaded', featuredServers.length, 'featured servers');
// Render featured servers
if (featuredServers.length === 0) {
featuredContainer.innerHTML = `
<div class="loading-spinner">
<i class="fas fa-info-circle fa-2x"></i>
<p>No featured servers</p>
</div>
`;
} else {
const featuredHTML = featuredServers.map((server, index) => {
console.log(`[FeaturedServers] Building featured card ${index + 1}:`, server.Name);
const escapedName = escapeHtml(server.Name || 'Unknown Server');
const escapedAddress = escapeHtml(server.Address || '');
const bannerUrl = server.img_Banner || 'https://via.placeholder.com/400x240/1e293b/ffffff?text=Server+Banner';
return `
<div class="featured-server-card">
<img
src="${bannerUrl}"
alt="${escapedName}"
class="featured-server-banner"
onerror="this.src='https://via.placeholder.com/400x240/1e293b/ffffff?text=Server'"
/>
<div class="featured-server-content">
<h3 class="featured-server-name">${escapedName}</h3>
<div class="featured-server-address">
<span class="server-address-text">${escapedAddress}</span>
<button class="copy-address-btn" onclick="copyServerAddress('${escapedAddress}', this)">
<i class="fas fa-copy"></i>
<span>Copy</span>
</button>
</div>
</div>
</div>
`;
}).join('');
featuredContainer.innerHTML = featuredHTML;
}
// Show "Coming Soon" for my servers
myServersContainer.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #94a3b8; font-size: 1.2rem;">
<p>Coming Soon</p>
</div>
`;
} catch (error) {
console.error('[FeaturedServers] Error loading servers:', error);
featuredContainer.innerHTML = `
<div class="loading-spinner">
<i class="fas fa-exclamation-triangle fa-2x" style="color: #ef4444;"></i>
<p>Failed to load servers</p>
<p style="font-size: 0.9rem; color: #64748b;">${error.message}</p>
</div>
`;
myServersContainer.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; height: 100%; color: #94a3b8; font-size: 1.2rem;">
<p>Coming Soon</p>
</div>
`;
}
}
/**
* Copy server address to clipboard
*/
async function copyServerAddress(address, button) {
try {
await navigator.clipboard.writeText(address);
// Visual feedback
const originalHTML = button.innerHTML;
button.classList.add('copied');
button.innerHTML = '<i class="fas fa-check"></i><span>Copied!</span>';
setTimeout(() => {
button.classList.remove('copied');
button.innerHTML = originalHTML;
}, 2000);
console.log('[FeaturedServers] Copied address:', address);
} catch (error) {
console.error('[FeaturedServers] Failed to copy address:', error);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = address;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
const originalHTML = button.innerHTML;
button.classList.add('copied');
button.innerHTML = '<i class="fas fa-check"></i><span>Copied!</span>';
setTimeout(() => {
button.classList.remove('copied');
button.innerHTML = originalHTML;
}, 2000);
} catch (err) {
console.error('[FeaturedServers] Fallback copy also failed:', err);
}
document.body.removeChild(textArea);
}
}
// Load featured servers when the featured page becomes visible
document.addEventListener('DOMContentLoaded', () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const featuredPage = document.getElementById('featured-page');
if (featuredPage && featuredPage.classList.contains('active')) {
loadFeaturedServers();
}
}
});
});
const featuredPage = document.getElementById('featured-page');
if (featuredPage) {
observer.observe(featuredPage, { attributes: true });
// Load immediately if already visible
if (featuredPage.classList.contains('active')) {
loadFeaturedServers();
}
}
});

View File

@@ -4,13 +4,15 @@ 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: 'de-DE', name: 'German (Germany)' },
{ code: 'es-ES', name: 'Spanish (Spain)' },
{ code: 'fr-FR', name: 'French (France)' },
{ code: 'pl-PL', name: 'Polish (Poland)' },
{ code: 'pt-BR', name: 'Portuguese (Brazil)' },
{ code: 'ru-RU', name: 'Russian (Russia)' },
{ code: 'sv-SE', name: 'Swedish (Sweden)' },
{ code: 'tr-TR', name: 'Turkish (Turkey)' },
{ code: 'pl-PL', name: 'Polish (Poland)' }
{ code: 'id-ID', name: 'Indonesian (Indonesia)' }
];
// Load single language file

View File

@@ -45,9 +45,17 @@ export function setupInstallation() {
export async function installGame() {
if (isDownloading || (installBtn && installBtn.disabled)) return;
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
let playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
const installPath = installPathInput ? installPathInput.value.trim() : '';
// Limit player name to 16 characters
if (playerName.length > 16) {
playerName = playerName.substring(0, 16);
if (installPlayerName) {
installPlayerName.value = playerName;
}
}
const selectedBranchRadio = document.querySelector('input[name="installBranch"]:checked');
const selectedBranch = selectedBranchRadio ? selectedBranchRadio.value : 'release';
@@ -72,8 +80,11 @@ export async function installGame() {
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true);
// Sync player name to both launcher and settings inputs
const playerNameInput = document.getElementById('playerName');
if (playerNameInput) playerNameInput.value = playerName;
const settingsPlayerName = document.getElementById('settingsPlayerName');
if (settingsPlayerName) settingsPlayerName.value = playerName;
resetInstallButton();
}, 2000);
}
@@ -125,8 +136,11 @@ function simulateInstallation(playerName) {
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true);
// Sync player name to both launcher and settings inputs
const playerNameInput = document.getElementById('playerName');
if (playerNameInput) playerNameInput.value = playerName;
const settingsPlayerName = document.getElementById('settingsPlayerName');
if (settingsPlayerName) settingsPlayerName.value = playerName;
resetInstallButton();
}, 2000);
}
@@ -188,7 +202,16 @@ export async function browseInstallPath() {
async function savePlayerName() {
try {
if (window.electronAPI && window.electronAPI.saveSettings) {
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
let playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
// Limit player name to 16 characters
if (playerName.length > 16) {
playerName = playerName.substring(0, 16);
if (installPlayerName) {
installPlayerName.value = playerName;
}
}
await window.electronAPI.saveSettings({ playerName });
}
} catch (error) {
@@ -246,9 +269,3 @@ document.addEventListener('DOMContentLoaded', async () => {
setupInstallation();
await checkGameStatusAndShowInterface();
});
window.browseInstallPath = browseInstallPath;
document.addEventListener('DOMContentLoaded', async () => {
setupInstallation();
await checkGameStatusAndShowInterface();
});

View File

@@ -4,23 +4,66 @@ import './launcher.js';
import './news.js';
import './mods.js';
import './players.js';
import './chat.js';
import './settings.js';
import './logs.js';
// Initialize i18n immediately (before DOMContentLoaded)
let i18nInitialized = false;
(async () => {
const savedLang = await window.electronAPI?.loadLanguage();
await i18n.init(savedLang);
i18nInitialized = true;
// Update language selector if DOM is already loaded
if (document.readyState === 'complete' || document.readyState === 'interactive') {
updateLanguageSelector();
}
})();
async function checkDiscordPopup() {
try {
const config = await window.electronAPI?.loadConfig();
if (!config || config.discordPopup === undefined || config.discordPopup === false) {
const modal = document.getElementById('discordPopupModal');
if (modal) {
const buttons = modal.querySelectorAll('.discord-popup-btn');
buttons.forEach(btn => btn.disabled = true);
setTimeout(() => {
modal.style.display = 'flex';
modal.classList.add('active');
setTimeout(() => {
buttons.forEach(btn => btn.disabled = false);
}, 2000);
}, 1000);
}
}
} catch (error) {
console.error('Failed to check Discord popup:', error);
}
}
window.closeDiscordPopup = function() {
const modal = document.getElementById('discordPopupModal');
if (modal) {
modal.classList.remove('active');
setTimeout(() => {
modal.style.display = 'none';
}, 300);
}
};
window.joinDiscord = async function() {
await window.electronAPI?.openExternal('https://discord.gg/hf2pdc');
try {
await window.electronAPI?.saveConfig({ discordPopup: true });
} catch (error) {
console.error('Failed to save Discord popup state:', error);
}
closeDiscordPopup();
};
function updateLanguageSelector() {
const langSelect = document.getElementById('languageSelect');
if (langSelect) {
@@ -51,32 +94,9 @@ function updateLanguageSelector() {
}
document.addEventListener('DOMContentLoaded', () => {
// Populate language selector (wait for i18n if needed)
if (i18nInitialized) {
updateLanguageSelector();
}
// Discord notification
const notification = document.getElementById('discordNotification');
if (notification) {
const dismissed = localStorage.getItem('discordNotificationDismissed');
if (!dismissed) {
setTimeout(() => {
notification.style.display = 'flex';
}, 3000);
} else {
notification.style.display = 'none';
}
}
checkDiscordPopup();
});
window.closeDiscordNotification = function() {
const notification = document.getElementById('discordNotification');
if (notification) {
notification.classList.add('hidden');
setTimeout(() => {
notification.style.display = 'none';
}, 300);
}
localStorage.setItem('discordNotificationDismissed', 'true');
};

View File

@@ -439,6 +439,13 @@ async function savePlayerName() {
return;
}
if (playerName.length > 16) {
const msg = window.i18n ? window.i18n.t('notifications.playerNameTooLong') : 'Player name must be 16 characters or less';
showNotification(msg, 'error');
settingsPlayerName.value = playerName.substring(0, 16);
return;
}
await window.electronAPI.saveUsername(playerName);
const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully';
showNotification(successMsg, 'success');

View File

@@ -63,8 +63,10 @@ function handleNavigation() {
navItems.forEach(item => {
item.addEventListener('click', () => {
const page = item.getAttribute('data-page');
showPage(`${page}-page`);
setActiveNav(page);
if (page) {
showPage(`${page}-page`);
setActiveNav(page);
}
});
});
}
@@ -843,7 +845,7 @@ function getErrorMessage(technicalMessage, errorType) {
case 'stall':
return 'Download stalled due to slow connection. Please retry.';
case 'file':
return 'Unable to save file. Check disk space and permissions. Please retry.';
return 'Unable to save file. Check permissions. Please retry.';
case 'permission':
return 'Permission denied. Check if launcher has write access. Please retry.';
case 'server':
@@ -972,7 +974,7 @@ function setupRetryButton() {
if (!currentDownloadState.retryData || currentDownloadState.errorType === 'jre') {
currentDownloadState.retryData = {
branch: 'release',
fileName: '4.pwr'
fileName: '7.pwr'
};
console.log('[UI] Created default PWR retry data:', currentDownloadState.retryData);
}
@@ -1040,7 +1042,7 @@ function setupRetryButton() {
} else {
currentDownloadState.retryData = {
branch: 'release',
fileName: '4.pwr'
fileName: '7.pwr'
};
}
console.log('[UI] Created default retry data:', currentDownloadState.retryData);
@@ -1100,7 +1102,10 @@ function getRetryContextMessage() {
}
}
// Make toggleMaximize globally available
window.openDiscordExternal = function() {
window.electronAPI?.openExternal('https://discord.gg/hf2pdc');
};
window.toggleMaximize = toggleMaximize;
document.addEventListener('DOMContentLoaded', setupUI);

View File

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

View File

@@ -119,7 +119,7 @@
"repairGame": "Spiel reparieren",
"reinstallGame": "Spieldateien neu installieren (behält Daten)",
"gpuPreference": "GPU-Präferenz",
"gpuHint": "Wähle deine bevorzugte GPU (Linux: betrifft DRI_PRIME)",
"gpuHint": "Funktion nur für Laptops; auf „Integriert“ stellen, wenn auf einem PC.",
"gpuAuto": "Auto",
"gpuIntegrated": "Integriert",
"gpuDedicated": "Dediziert",
@@ -211,40 +211,7 @@
"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"
"hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden"
},
"confirm": {
"defaultTitle": "Aktion bestätigen",

View File

@@ -119,7 +119,7 @@
"repairGame": "Repair Game",
"reinstallGame": "Reinstall game files (preserves data)",
"gpuPreference": "GPU Preference",
"gpuHint": "Select your preferred GPU (Linux: affects DRI_PRIME)",
"gpuHint": "Laptop-only feature; set to Integrated if on PC",
"gpuAuto": "Auto",
"gpuIntegrated": "Integrated",
"gpuDedicated": "Dedicated",

View File

@@ -119,7 +119,7 @@
"repairGame": "Reparar juego",
"reinstallGame": "Reinstalar archivos del juego (conserva los datos)",
"gpuPreference": "Preferencia de GPU",
"gpuHint": "Selecciona tu GPU preferida (Linux: afecta DRI_PRIME)",
"gpuHint": "Función exclusiva para computadora portátil; configúrela como Integrada si está en una PC",
"gpuAuto": "Automático",
"gpuIntegrated": "Integrada",
"gpuDedicated": "Dedicada",
@@ -131,6 +131,8 @@
"closeLauncher": "Comportamiento del Launcher",
"closeOnStart": "Cerrar Launcher al iniciar el juego",
"closeOnStartDescription": "Cierra automáticamente el launcher después de que Hytale se haya iniciado",
"hwAccel": "Aceleración por Hardware",
"hwAccelDescription": "Habilitar aceleración por hardware para el launcher",
"gameBranch": "Rama del Juego",
"branchRelease": "Lanzamiento",
"branchPreRelease": "Pre-Lanzamiento",
@@ -207,7 +209,9 @@
"modsDownloadFailed": "Error al descargar mod: {error}",
"modsToggleFailed": "Error al alternar mod: {error}",
"modsDeleteFailed": "Error al eliminar mod: {error}",
"modsModNotFound": "Información del mod no encontrada"
"modsModNotFound": "Información del mod no encontrada",
"hwAccelSaved": "Configuración de aceleración por hardware guardada",
"hwAccelSaveFailed": "Error al guardar la configuración de aceleración por hardware"
},
"confirm": {
"defaultTitle": "Confirmar acción",

View File

@@ -119,7 +119,7 @@
"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)",
"gpuHint": "Fonctionnalité exclusive aux ordinateurs portables; à définir sur Intégré sur PC",
"gpuAuto": "Auto",
"gpuIntegrated": "Intégré",
"gpuDedicated": "Dédié",
@@ -198,38 +198,53 @@
"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"
"uuidDeleteFailed": "Échec de la suppression de l'UUID",
"uuidDeleteSuccess": "UUID supprimé avec succès!",
"modsDownloading": "Téléchargement de {name}...",
"modsTogglingMod": "Basculement du mod...",
"modsDeletingMod": "Suppression du mod...",
"modsLoadingMods": "Chargement des mods depuis CurseForge...",
"modsInstalledSuccess": "{name} installé avec succès! 🎉",
"modsDeletedSuccess": "{name} supprimé avec succès",
"modsDownloadFailed": "Échec du téléchargement du mod: {error}",
"modsToggleFailed": "Échec du basculement du mod: {error}",
"modsDeleteFailed": "Échec de la suppression du mod: {error}",
"modsModNotFound": "Informations du mod introuvables",
"hwAccelSaved": "Paramètre d'accélération matérielle sauvegardé",
"hwAccelSaveFailed": "Échec de la sauvegarde du paramètre d'accélération matérielle"
},
"confirm": {
"defaultTitle": "Confirmer l'action",
"regenerateUuidTitle": "Générer un nouvel UUID",
"regenerateUuidMessage": "Êtes-vous sûr de vouloir générer un nouvel UUID? Cela changera votre identité de joueur.",
"regenerateUuidButton": "Générer",
"setCustomUuidTitle": "Définir UUID personnalisé",
"setCustomUuidMessage": "Êtes-vous sûr de vouloir définir cet UUID personnalisé? Cela changera votre identité de joueur.",
"setCustomUuidButton": "Définir UUID",
"deleteUuidTitle": "Supprimer UUID",
"deleteUuidMessage": "Êtes-vous sûr de vouloir supprimer l'UUID de \"{username}\"? Cette action est irréversible.",
"deleteUuidButton": "Supprimer",
"uninstallGameTitle": "Désinstaller le jeu",
"uninstallGameMessage": "Êtes-vous sûr de vouloir désinstaller Hytale? Tous les fichiers du jeu seront supprimés.",
"uninstallGameButton": "Désinstaller"
},
"progress": {
"initializing": "Initialisation...",
"downloading": "Téléchargement...",
"installing": "Installation...",
"extracting": "Extraction...",
"verifying": "Vérification...",
"switchingProfile": "Changement de profil...",
"profileSwitched": "Profil changé!",
"startingGame": "Démarrage du jeu...",
"launching": "LANCEMENT...",
"uninstallingGame": "Désinstallation du jeu...",
"gameUninstalled": "Jeu désinstallé avec succès!",
"uninstallFailed": "Échec de la désinstallation: {error}",
"startingUpdate": "Démarrage de la mise à jour obligatoire du jeu...",
"installationComplete": "Installation terminée avec succès!",
"installationFailed": "Échec de l'installation: {error}",
"installingGameFiles": "Installation des fichiers du jeu...",
"installComplete": "Installation terminée!"
}
}

250
GUI/locales/id-ID.json Normal file
View File

@@ -0,0 +1,250 @@
{
"nav": {
"play": "Main",
"mods": "Mod",
"news": "Berita",
"chat": "Obrolan Pemain",
"settings": "Pengaturan"
},
"header": {
"playersLabel": "Pemain:",
"manageProfiles": "Kelola Profil",
"defaultProfile": "Default"
},
"install": {
"title": "LAUNCHER GRATIS UNTUK DIMAINKAN",
"playerName": "Nama Pemain",
"playerNamePlaceholder": "Masukkan namamu",
"gameBranch": "Versi Game",
"releaseVersion": "Rilis (Stabil)",
"preReleaseVersion": "Pra-Rilis (Eksperimental)",
"customInstallation": "Instalasi Kustom",
"installationFolder": "Folder Instalasi",
"pathPlaceholder": "Lokasi default",
"browse": "Telusuri",
"installButton": "INSTAL HYTALE",
"installing": "MENGINSTAL..."
},
"play": {
"ready": "SIAP BERMAIN",
"subtitle": "Luncurkan Hytale dan mulai petualanganmu",
"playButton": "MAIN HYTALE",
"latestNews": "BERITA TERBARU",
"viewAll": "LIHAT SEMUA",
"checking": "MEMERIKSA...",
"play": "MAIN"
},
"mods": {
"searchPlaceholder": "Cari mod...",
"myMods": "MOD SAYA",
"previous": "SEBELUMNYA",
"next": "BERIKUTNYA",
"page": "Halaman",
"of": "dari",
"modalTitle": "MOD SAYA",
"noModsFound": "Mod Tidak Ditemukan",
"noModsFoundDesc": "Coba sesuaikan pencarianmu",
"noModsInstalled": "Tidak ada Mod Terinstal",
"noModsInstalledDesc": "Tambahkan mod dari CurseForge atau impor file lokal",
"view": "LIHAT",
"install": "INSTAL",
"installed": "TERINSTAL",
"enable": "AKTIFKAN",
"disable": "NONAKTIFKAN",
"active": "AKTIF",
"disabled": "NONAKTIF",
"delete": "Hapus mod",
"noDescription": "Tidak ada deskripsi tersedia",
"confirmDelete": "Apakah kamu yakin ingin menghapus \"{name}\"?",
"confirmDeleteDesc": "Tindakan ini tidak dapat dibatalkan.",
"confirmDeletion": "Konfirmasi Penghapusan",
"apiKeyRequired": "Kunci API Diperlukan",
"apiKeyRequiredDesc": "Kunci API CurseForge diperlukan untuk menelusuri mod"
},
"news": {
"title": "SEMUA BERITA",
"readMore": "Baca Selengkapnya"
},
"chat": {
"title": "OBROLAN PEMAIN",
"pickColor": "Warna",
"inputPlaceholder": "Ketik pesanmu...",
"send": "Kirim",
"online": "aktif",
"charCounter": "{current}/{max}",
"secureChat": "Obrolan aman - Tautan disensor",
"joinChat": "Gabung Obrolan",
"chooseUsername": "Pilih nama pengguna untuk bergabung ke Obrolan Pemain",
"username": "Nama Pengguna",
"usernamePlaceholder": "Masukkan nama penggunamu...",
"usernameHint": "3-20 karakter, huruf, angka, - dan _ saja",
"joinButton": "Gabung Obrolan",
"colorModal": {
"title": "Kustomisasi Warna Nama Pengguna",
"chooseSolid": "Pilih warna solid:",
"customColor": "Warna kustom:",
"preview": "Pratinjau:",
"previewUsername": "Nama Pengguna",
"apply": "Terapkan Warna"
}
},
"settings": {
"title": "PENGATURAN",
"java": "Runtime Java",
"useCustomJava": "Gunakan lokasi Java Kustom",
"javaDescription": "Ganti runtime Java bawaan dengan instalasi milikmu",
"javaPath": "Lokasi Eksekutabel Java",
"javaPathPlaceholder": "Pilih lokasi Java...",
"javaBrowse": "Telusuri",
"javaHint": "Pilih folder instalasi Java (mendukung Windows, Mac, Linux)",
"discord": "Integrasi Discord",
"enableRPC": "Aktifkan Discord Rich Presence",
"discordDescription": "Tampilkan aktivitas launchermu di Discord",
"game": "Opsi Game",
"playerName": "Nama Pemain",
"playerNamePlaceholder": "Masukkan nama pemainmu",
"playerNameHint": "Nama ini akan digunakan di dalam game (1-16 karakter)",
"openGameLocation": "Buka Lokasi Game",
"openGameLocationDesc": "Buka folder instalasi game",
"account": "Manajemen UUID Pemain",
"currentUUID": "UUID Saat Ini",
"uuidPlaceholder": "Memuat UUID...",
"copyUUID": "Salin UUID",
"regenerateUUID": "Regenerasi UUID",
"uuidHint": "Pengidentifikasi pemain unikmu untuk nama pengguna ini",
"manageUUIDs": "Kelola Semua UUID",
"manageUUIDsDesc": "Lihat dan kelola semua UUID pemain",
"language": "Bahasa",
"selectLanguage": "Pilih Bahasa",
"repairGame": "Perbaiki Game",
"reinstallGame": "Instal ulang file game (tetap menyimpan data)",
"gpuPreference": "Preferensi GPU",
"gpuHint": "Fitur khusus laptop; setel ke Terintegrasi jika di PC",
"gpuAuto": "Otomatis",
"gpuIntegrated": "Terintegrasi",
"gpuDedicated": "Terdedikasi",
"logs": "LOG SISTEM",
"logsCopy": "Salin",
"logsRefresh": "Segarkan",
"logsFolder": "Buka Folder",
"logsLoading": "Memuat log...",
"closeLauncher": "Perilaku Launcher",
"closeOnStart": "Tutup launcher saat game dimulai",
"closeOnStartDescription": "Tutup launcher secara otomatis setelah Hytale diluncurkan",
"hwAccel": "Akselerasi Perangkat Keras",
"hwAccelDescription": "Aktifkan akselerasi perangkat keras untuk launcher`",
"gameBranch": "Cabang Game",
"branchRelease": "Rilis",
"branchPreRelease": "Pra-Rilis",
"branchHint": "Beralih antara rilis stabil dan versi pra-rilis eksperimental",
"branchWarning": "Mengubah cabang akan mengunduh dan menginstal versi game yang berbeda",
"branchSwitching": "Beralih ke {branch}...",
"branchSwitched": "Berhasil beralih ke {branch}!",
"installRequired": "Instalasi Diperlukan",
"branchInstallConfirm": "Game akan diinstal untuk cabang {branch}. Lanjutkan?"
},
"uuid": {
"modalTitle": "Manajemen UUID",
"currentUserUUID": "UUID Pengguna Saat Ini",
"allPlayerUUIDs": "Semua UUID Pemain",
"generateNew": "Hasilkan UUID Baru",
"loadingUUIDs": "Memuat UUID...",
"setCustomUUID": "Setel UUID Kustom",
"customPlaceholder": "Masukkan UUID kustom (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "Setel UUID",
"warning": "Peringatan: Menyetel UUID secara kustom akan mengubah identitas pemainmu saat ini",
"copyTooltip": "Salin UUID",
"regenerateTooltip": "Hasilkan UUID Baru"
},
"profiles": {
"modalTitle": "Kelola Profil",
"newProfilePlaceholder": "Nama Profil Baru",
"createProfile": "Buat Profil"
},
"discord": {
"notificationText": "Gabung komunitas Discord kami!",
"joinButton": "Gabung Discord"
},
"common": {
"confirm": "Konfirmasi",
"cancel": "Batal",
"save": "Simpan",
"close": "Tutup",
"delete": "Hapus",
"edit": "Edit",
"loading": "Memuat...",
"apply": "Terapkan",
"install": "Instal"
},
"notifications": {
"gameDataNotFound": "Kesalahan: Data game tidak ditemukan",
"gameUpdatedSuccess": "Game berhasil diperbarui! 🎉",
"updateFailed": "Pembaruan gagal: {error}",
"updateError": "Kesalahan pembaruan: {error}",
"discordEnabled": "Discord Rich Presence diaktifkan",
"discordDisabled": "Discord Rich Presence dinonaktifkan",
"discordSaveFailed": "Gagal menyimpan pengaturan Discord",
"playerNameRequired": "Silakan masukkan nama pemain yang valid",
"playerNameSaved": "Nama pemain berhasil disimpan",
"playerNameSaveFailed": "Gagal menyimpan nama pemain",
"uuidCopied": "UUID disalin ke papan klip!",
"uuidCopyFailed": "Gagal menyalin UUID",
"uuidRegenNotAvailable": "Regenerasi UUID tidak tersedia",
"uuidRegenFailed": "Gagal meregenerasi UUID",
"uuidGenerated": "UUID baru berhasil dihasilkan!",
"uuidGeneratedShort": "UUID baru dihasilkan!",
"uuidGenerateFailed": "Gagal menghasilkan UUID baru",
"uuidRequired": "Silakan masukkan UUID",
"uuidInvalidFormat": "Format UUID tidak valid",
"uuidSetFailed": "Gagal menyetel UUID kustom",
"uuidSetSuccess": "UUID kustom berhasil disetel!",
"uuidDeleteFailed": "Gagal menghapus UUID",
"uuidDeleteSuccess": "UUID berhasil dihapus!",
"modsDownloading": "Mengunduh {name}...",
"modsTogglingMod": "Beralih mod...",
"modsDeletingMod": "Menghapus mod...",
"modsLoadingMods": "Memuat mod dari CurseForge...",
"modsInstalledSuccess": "{name} berhasil diinstal! 🎉",
"modsDeletedSuccess": "{name} berhasil dihapus",
"modsDownloadFailed": "Gagal mengunduh mod: {error}",
"modsToggleFailed": "Gagal beralih mod: {error}",
"modsDeleteFailed": "Gagal menghapus mod: {error}",
"modsModNotFound": "Informasi mod tidak ditemukan",
"hwAccelSaved": "Pengaturan akselerasi perangkat keras disimpan",
"hwAccelSaveFailed": "Gagal menyimpan pengaturan akselerasi perangkat keras"
},
"confirm": {
"defaultTitle": "Konfirmasi tindakan",
"regenerateUuidTitle": "Hasilkan UUID baru",
"regenerateUuidMessage": "Apakah kamu yakin ingin menghasilkan UUID baru? Ini akan mengubah identitas pemainmu.",
"regenerateUuidButton": "Hasilkan",
"setCustomUuidTitle": "Setel UUID kustom",
"setCustomUuidMessage": "Apakah kamu yakin ingin menyetel UUID kustom ini? Ini akan mengubah identitas pemainmu.",
"setCustomUuidButton": "Setel UUID",
"deleteUuidTitle": "Hapus UUID",
"deleteUuidMessage": "Apakah kamu yakin ingin menghapus UUID untuk \"{username}\"? Tindakan ini tidak dapat dibatalkan.",
"deleteUuidButton": "Hapus",
"uninstallGameTitle": "Hapus instalasi game",
"uninstallGameMessage": "Apakah kamu yakin ingin menghapus instalasi Hytale? Semua file game akan dihapus.",
"uninstallGameButton": "Hapus Instalasi"
},
"progress": {
"initializing": "Menginisialisasi...",
"downloading": "Mengunduh...",
"installing": "Menginstal...",
"extracting": "Mengekstrak...",
"verifying": "Memverifikasi...",
"switchingProfile": "Beralih profil...",
"profileSwitched": "Profil dialihkan!",
"startingGame": "Memulai game...",
"launching": "MELUNCURKAN...",
"uninstallingGame": "Menghapus instalasi game...",
"gameUninstalled": "Instalasi game berhasil dihapus!",
"uninstallFailed": "Penghapusan instalasi gagal: {error}",
"startingUpdate": "Memulai pembaruan game wajib...",
"installationComplete": "Instalasi berhasil diselesaikan!",
"installationFailed": "Instalasi gagal: {error}",
"installingGameFiles": "Menginstal file game...",
"installComplete": "Instalasi selesai!"
}
}

View File

@@ -4,19 +4,20 @@
"mods": "Mody",
"news": "Wiadomości",
"chat": "Chat z graczami",
"settings": "Ustawienia",
"skins": "Skiny"
"settings": "Ustawienia"
},
"header": {
"playersLabel": "Graczy:",
"manageProfiles": "Zarządzaj Profilami",
"defaultProfile": "Domyślny",
"f2p": "FREE TO PLAY"
"defaultProfile": "Domyślny"
},
"install": {
"title": "FREE TO PLAY LAUNCHER",
"title": "DARMOWY LAUNCHER",
"playerName": "Nazwa Gracza",
"playerNamePlaceholder": "Wprowadź Nazwę",
"gameBranch": "Wersja Gry",
"releaseVersion": "Wydanie (Stabilna)",
"preReleaseVersion": "Przed-Wydaniem (Eksperymentalna)",
"customInstallation": "Dostosuj Instalacje",
"installationFolder": "Folder docelowy",
"pathPlaceholder": "Domyślna lokalizacja",
@@ -56,7 +57,9 @@
"noDescription": "Brak opisu",
"confirmDelete": "Czy na pewno chcesz usunąć \"{name}\"?",
"confirmDeleteDesc": "Tej czynności nie można cofnąć.",
"confirmDeletion": "Potwierdź"
"confirmDeletion": "Potwierdź",
"apiKeyRequired": "Wymagany Klucz API",
"apiKeyRequiredDesc": "Klucz API CurseForge jest potrzebny do przeglądania modów"
},
"news": {
"title": "WSZYSTKIE WIADOMOŚCI",
@@ -116,15 +119,29 @@
"repairGame": "Napraw Grę",
"reinstallGame": "Zainstaluj ponownie pliki gry (zachowuje dane)",
"gpuPreference": "Preferencje GPU",
"gpuHint": "Wybierz preferowany procesor graficzny (Linux: wpływa na DRI_PRIME)",
"gpuHint": "Funkcja dostępna tylko na laptopie; ustaw na Zintegrowaną, jeśli na komputerze PC",
"gpuAuto": "Auto",
"gpuIntegrated": "Zintegrowana",
"gpuDedicated": "Dedykowana",
"logs": "SYSTEM LOGS",
"logs": "DZIENNIKI SYSTEMOWE",
"logsCopy": "Kopiuj",
"logsRefresh": "Odśwież",
"logsFolder": "Otwórz Folder",
"logsLoading": "Ładowanie logów..."
"logsLoading": "Ładowanie logów...",
"closeLauncher": "Zachowanie Launchera",
"closeOnStart": "Zamknij Launcher przy starcie gry",
"closeOnStartDescription": "Automatycznie zamknij launcher po uruchomieniu Hytale",
"hwAccel": "Przyspieszenie Sprzętowe",
"hwAccelDescription": "Włącz przyspieszenie sprzętowe dla launchera",
"gameBranch": "Gałąź Gry",
"branchRelease": "Wydanie",
"branchPreRelease": "Przed-Wydaniem",
"branchHint": "Przełączaj między stabilnym wydaniem a eksperymentalną wersją przed-wydaniem",
"branchWarning": "Zmiana gałęzi spowoduje pobranie i instalację innej wersji gry",
"branchSwitching": "Przełączanie na {branch}...",
"branchSwitched": "Pomyślnie przełączono na {branch}!",
"installRequired": "Wymagana Instalacja",
"branchInstallConfirm": "Gra zostanie zainstalowana dla gałęzi {branch}. Kontynuować?"
},
"uuid": {
"modalTitle": "Zarządzanie UUID",
@@ -148,10 +165,6 @@
"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",
@@ -160,7 +173,8 @@
"delete": "Usuń",
"edit": "Edytuj",
"loading": "Ładowanie...",
"apply": "Zastosuj"
"apply": "Zastosuj",
"install": "Zainstaluj"
},
"notifications": {
"gameDataNotFound": "Błąd: Nie znaleziono danych gry",
@@ -195,7 +209,9 @@
"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"
"modsModNotFound": "Nie znaleziono informacji o modzie",
"hwAccelSaved": "Zapisano ustawienie przyspieszenia sprzętowego",
"hwAccelSaveFailed": "Nie udało się zapisać ustawienia przyspieszenia sprzętowego"
},
"confirm": {
"defaultTitle": "Potwierdź działanie",

View File

@@ -14,9 +14,11 @@
"install": {
"title": "LANÇADOR JOGO GRATUITO",
"playerName": "Nome do Jogador",
"playerNamePlaceholder": "Digite seu nome", "gameBranch": "Versão do Jogo",
"playerNamePlaceholder": "Digite seu nome",
"gameBranch": "Versão do Jogo",
"releaseVersion": "Lançamento (Estável)",
"preReleaseVersion": "Pré-Lançamento (Experimental)", "customInstallation": "Instalação Personalizada",
"preReleaseVersion": "Pré-Lançamento (Experimental)",
"customInstallation": "Instalação Personalizada",
"installationFolder": "Pasta de Instalação",
"pathPlaceholder": "Local padrão",
"browse": "Procurar",
@@ -117,7 +119,7 @@
"repairGame": "Reparar jogo",
"reinstallGame": "Reinstalar arquivos do jogo (mantém os dados)",
"gpuPreference": "Preferência de GPU",
"gpuHint": "Selecione sua GPU preferida (Linux: afeta o DRI_PRIME)",
"gpuHint": "Recurso exclusivo para laptops; defina como Integrado se estiver em um PC.",
"gpuAuto": "Automático",
"gpuIntegrated": "Integrada",
"gpuDedicated": "Dedicada",
@@ -129,6 +131,8 @@
"closeLauncher": "Comportamento do Lançador",
"closeOnStart": "Fechar Lançador ao iniciar o jogo",
"closeOnStartDescription": "Fechar automaticamente o lançador após o Hytale ter sido iniciado",
"hwAccel": "Aceleração de Hardware",
"hwAccelDescription": "Ativar aceleração de hardware para o lançador",
"gameBranch": "Versão do Jogo",
"branchRelease": "Lançamento",
"branchPreRelease": "Pré-Lançamento",
@@ -161,7 +165,6 @@
"notificationText": "Junte-se à nossa comunidade do Discord!",
"joinButton": "Entrar no Discord"
},
"common": {
"confirm": "Confirmar",
"cancel": "Cancelar",
@@ -206,7 +209,9 @@
"modsDownloadFailed": "Falha ao baixar mod: {error}",
"modsToggleFailed": "Falha ao alternar mod: {error}",
"modsDeleteFailed": "Falha ao excluir mod: {error}",
"modsModNotFound": "Informações do mod não encontradas"
"modsModNotFound": "Informações do mod não encontradas",
"hwAccelSaved": "Configuração de aceleração de hardware salva",
"hwAccelSaveFailed": "Falha ao salvar configuração de aceleração de hardware"
},
"confirm": {
"defaultTitle": "Confirmar ação",

250
GUI/locales/ru-RU.json Normal file
View File

@@ -0,0 +1,250 @@
{
"nav": {
"play": "Играть",
"mods": "Моды",
"news": "Новости",
"chat": "Чат игроков",
"settings": "Настройки"
},
"header": {
"playersLabel": "Игроки:",
"manageProfiles": "Управлять профилями:",
"defaultProfile": "По умолчанию"
},
"install": {
"title": "FREE TO PLAY LAUNCHER",
"playerName": "Ник игрока",
"playerNamePlaceholder": "Введите ваш ник",
"gameBranch": "Версия игры",
"releaseVersion": "Релиз (Стабильная)",
"preReleaseVersion": "Пре-Релиз (Экспериментально)",
"customInstallation": "Модифицированная установка",
"installationFolder": "Папка установки",
"pathPlaceholder": "Путь по умолчанию",
"browse": "Обзор",
"installButton": "УСТАНОВИТЬ HYTALE",
"installing": "УСТАНОВКА..."
},
"play": {
"ready": "ГОТОВ К ИГРЕ",
"subtitle": "Запусти Hytale и приготовься к приключению!",
"playButton": "ЗАПУСТИТЬ HYTALE",
"latestNews": "ПОСЛЕДНИЕ НОВОСТИ",
"viewAll": "ПОСМОТРЕТЬ ВСЁ",
"checking": "ПРОВЕРКА...",
"play": "ЗАПУСТИТЬ"
},
"mods": {
"searchPlaceholder": "Искать моды...",
"myMods": "Мои моды",
"previous": "Предыдущая",
"next": "Вперёд",
"page": "Страница",
"of": "",
"modalTitle": "МОИ МОДЫ",
"noModsFound": "Моды не найдены",
"noModsFoundDesc": "Попробуйте изменить свой запрос",
"noModsInstalled": "Нет установленных модов",
"noModsInstalledDesc": "Добавьте моды с CurseForge или импортируйте свои!",
"view": "Посмотреть",
"install": "Установить",
"installed": "УСТАНОВЛЕННЫЕ",
"enable": "ВКЛЮЧИТЬ",
"disable": "ВЫКЛЮЧИТЬ",
"active": "ВКЛЮЧЁН",
"disabled": "ВЫКЛЮЧЕН",
"delete": "Удалить мод",
"noDescription": "Нет доступного описания",
"confirmDelete": "Вы точно уверены, что хотите удалить \"{name}\"?",
"confirmDeleteDesc": "Это действие не отменить.",
"confirmDeletion": "Подтвердите удаление",
"apiKeyRequired": "Требуется ключ API",
"apiKeyRequiredDesc": "Ключ CurseForge API требуется для просмотра модов"
},
"news": {
"title": "ВСЕ НОВОСТИ",
"readMore": "Читать дальше"
},
"chat": {
"title": "ЧАТ ИГРОКОВ",
"pickColor": "Цвет",
"inputPlaceholder": "Введите своё сообщение...",
"send": "Отправить",
"online": "онлайн",
"charCounter": "{current}/{max}",
"secureChat": "Безопасный чат - все ссылки зацензурены",
"joinChat": "Присоединиться к чату",
"chooseUsername": "Выберите имя пользователя для входа в чат игроков",
"username": "Ник",
"usernamePlaceholder": "Введите ваш ник...",
"usernameHint": "3-20 символов, букв, цифр, только - и _",
"joinButton": "Присоединиться к чату",
"colorModal": {
"title": "Выберите цвет ника",
"chooseSolid": "Выберите цвет:",
"customColor": "Модифицированный цвет:",
"preview": "Предварительный просмотр:",
"previewUsername": "Ник",
"apply": "Применить цвет"
}
},
"settings": {
"title": "НАСТРОЙКИ",
"java": "Java Runtime",
"useCustomJava": "Укажите свой путь Java",
"javaDescription": "Переопределить встроенный Java Runtime с вашей установкой",
"javaPath": "Путь исполняемого файла Java",
"javaPathPlaceholder": "Выберите путь Java...",
"javaBrowse": "Обзор",
"javaHint": "Выберите папку установки Java (поддерживается Windows, Mac, Linux)",
"discord": "Интеграция Discord",
"enableRPC": "Включить Discord Rich Presence",
"discordDescription": "Показывать вашу активность лаунчера в Discord",
"game": "Настройки игры",
"playerName": "Ник игрока",
"playerNamePlaceholder": "Введите ваш ник",
"playerNameHint": "Этот ник будет использован в игре (1-16 символов)",
"openGameLocation": "Открыть местоположение игры",
"openGameLocationDesc": "Открыть папку установки игры",
"account": "Управление UUID игрока",
"currentUUID": "Текущий UUID",
"uuidPlaceholder": "Загрузка UUID...",
"copyUUID": "Копировать UUID",
"regenerateUUID": "Перегенерировать UUID",
"uuidHint": "Уникальный идентификатор игрока для этого ника",
"manageUUIDs": "Управление всеми UUID",
"manageUUIDsDesc": "Смотреть и управлять всеми UUID игрока",
"language": "Язык",
"selectLanguage": "Выберите язык",
"repairGame": "Починить игру",
"reinstallGame": "Переустановить файлы игры (сохраняет данные)",
"gpuPreference": "Предпочтение GPU",
"gpuHint": "Функция доступна только на ноутбуках; при использовании на ПК выберите встроенную видеокарту.",
"gpuAuto": "Автоматический выбор",
"gpuIntegrated": "Интегрированная видеокарта",
"gpuDedicated": "Дискретная видеокарта",
"logs": "ЛОГИ",
"logsCopy": "Копировать",
"logsRefresh": "Обновить",
"logsFolder": "Открыть папку",
"logsLoading": "Загрузка логов...",
"closeLauncher": "Поведение лаунчера",
"closeOnStart": "Закрыть лаунчер при старте игры",
"closeOnStartDescription": "Автоматически закрыть лаунчер после запуска Hytale",
"hwAccel": "Аппаратное ускорение",
"hwAccelDescription": "Включить аппаратное ускорение для лаунчера",
"gameBranch": "Ветка игры",
"branchRelease": "Релиз",
"branchPreRelease": "Пре-Релиз",
"branchHint": "Переключает между релизом и пре-релизом игры",
"branchWarning": "Изменение ветки скачает и установит другую версию игры",
"branchSwitching": "Переключение на {branch}...",
"branchSwitched": "Переключение на {branch} выполнено успешно!",
"installRequired": "Необходима установка",
"branchInstallConfirm": "Игра будет установлена для ветки {branch}. Продолжить?"
},
"uuid": {
"modalTitle": "Управление UUID",
"currentUserUUID": "UUID текущего пользователя",
"allPlayerUUIDs": "UUID всех игроков",
"generateNew": "Сгенерировать новый UUID",
"loadingUUIDs": "Загрузка UUID...",
"setCustomUUID": "Установить кастомный UUID",
"customPlaceholder": "Ввести кастомный UUID (форматы: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)",
"setUUID": "Установить UUID",
"warning": "Внимание! Установка кастомного UUID изменит вашу текущую личность игрока!",
"copyTooltip": "Скопировать UUID",
"regenerateTooltip": "Сгенерировать новый UUID"
},
"profiles": {
"modalTitle": "Управление профилями",
"newProfilePlaceholder": "Новое имя профиля",
"createProfile": "Создать профиль"
},
"discord": {
"notificationText": "Присоединитесь к нашему сообществу в Discord!",
"joinButton": "Присоединиться к Discord"
},
"common": {
"confirm": "Подтвердить",
"cancel": "Отменить",
"save": "Сохранить",
"close": "Закрыть",
"delete": "Удалить",
"edit": "Редактировать",
"loading": "Загружается...",
"apply": "Применить",
"install": "Установить"
},
"notifications": {
"gameDataNotFound": "Ошибка: данные игры не найдены",
"gameUpdatedSuccess": "Игра успешно обновлена! Ура! 🎉",
"updateFailed": "Обновление прервалось с ошибкой: {error}",
"updateError": "Ошибка обновления: {error}",
"discordEnabled": "Discord Rich Presence включен",
"discordDisabled": "Discord Rich Presence выключен",
"discordSaveFailed": "Не удалось сохранить настройку Discord",
"playerNameRequired": "Пожалуйста, введите действительное имя игрока",
"playerNameSaved": "Имя игрока успешно сохранено!",
"playerNameSaveFailed": "Не удалось сохранить имя игрока",
"uuidCopied": "UUID скопирован в буфер обмена!",
"uuidCopyFailed": "Не удалось скопировать UUID",
"uuidRegenNotAvailable": "UUID перегенерация к сожалению не доступна",
"uuidRegenFailed": "Не удалось перегенерировать UUID",
"uuidGenerated": "Новый UUID сгенерирован успешно!",
"uuidGeneratedShort": "Новый UUID сгенерирован!",
"uuidGenerateFailed": "Не получилось сгенерировать новый UUID",
"uuidRequired": "Пожалуйста введите UUID",
"uuidInvalidFormat": "Неправильный формат UUID",
"uuidSetFailed": "Не удалось поставить кастомный UUID",
"uuidSetSuccess": "Кастомный UUID успешно установлен!",
"uuidDeleteFailed": "Не удалось удалить UUID",
"uuidDeleteSuccess": "Удаление UUID успешно завершено!",
"modsDownloading": "Скачивание {name}...",
"modsTogglingMod": "Включение мода...",
"modsDeletingMod": "Удаление мода...",
"modsLoadingMods": "Загрузка модов с CurseForge...",
"modsInstalledSuccess": "{name} успешно установлен! 🎉",
"modsDeletedSuccess": "{name} удалён успешно!",
"modsDownloadFailed": "Не получилось скачать мод: {error}",
"modsToggleFailed": "Не получилось включить мод: {error}",
"modsDeleteFailed": "Не получилось удалить мод: {error}",
"modsModNotFound": "Информация по моду не найдена",
"hwAccelSaved": "Настройка аппаратного ускорения сохранена!",
"hwAccelSaveFailed": "Не удалось сохранить настройку аппаратного ускорения"
},
"confirm": {
"defaultTitle": "Подтвердить действие",
"regenerateUuidTitle": "Сгенерировать новый UUID",
"regenerateUuidMessage": "Вы уверены, что хотите сгенерировать новый UUID? Генерация нового UUID изменит вашу текущую личность игрока!",
"regenerateUuidButton": "Сгенерировать",
"setCustomUuidTitle": "Установить кастомный UUID",
"setCustomUuidMessage": "Вы уверены, что хотите установить кастомный UUID? Установка кастомного UUID изменит вашу текущую личность игрока!",
"setCustomUuidButton": "Установить UUID",
"deleteUuidTitle": "Удалить UUID",
"deleteUuidMessage": "Вы уверены, что хотите удалить UUID для \"{username}\"? Это действие необратимо!",
"deleteUuidButton": "Удалить",
"uninstallGameTitle": "Удалить игру",
"uninstallGameMessage": "Вы уверены, что хотите удалить Hytale? Все данные игры будут безвозвратно удалены!",
"uninstallGameButton": "Удалить"
},
"progress": {
"initializing": "Инициализация...",
"downloading": "Скачивание...",
"installing": "Установка...",
"extracting": "Извлечение...",
"verifying": "Проверка...",
"switchingProfile": "Смена профиля...",
"profileSwitched": "Профиль сменён!",
"startingGame": "Запуск игры...",
"launching": "ЗАПУСК...",
"uninstallingGame": "Удаление игры...",
"gameUninstalled": "Игра успешно удалена!",
"uninstallFailed": "Удаление игры прервано с ошибкой: {error}",
"startingUpdate": "Начало обязательного обновления игры...",
"installationComplete": "Установка успешно завершена!",
"installationFailed": "Установка прервана с ошибкой: {error}",
"installingGameFiles": "Установка файлов игры...",
"installComplete": "Установка завершена!"
}
}

View File

@@ -119,7 +119,7 @@
"repairGame": "Reparera spel",
"reinstallGame": "Ominstallera spelfiler (bevarar data)",
"gpuPreference": "GPU-preferens",
"gpuHint": "Välj din föredragna GPU (Linux: påverkar DRI_PRIME)",
"gpuHint": "Endast för bärbar dator; inställd på Integrerad om den är på datorn",
"gpuAuto": "Auto",
"gpuIntegrated": "Integrerad",
"gpuDedicated": "Dedikerad",
@@ -211,40 +211,7 @@
"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"
"hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning"
},
"confirm": {
"defaultTitle": "Bekräfta åtgärd",

View File

@@ -119,7 +119,7 @@
"repairGame": "Oyunu Onarı",
"reinstallGame": "Oyun dosyalarını yeniden kur (veri korur)",
"gpuPreference": "GPU Tercihi",
"gpuHint": "Tercih ettiğiniz GPU'yu seçin (Linux: DRI_PRIME'ı etkiler)",
"gpuHint": "Sadece dizüstü bilgisayarlarda bulunan bir özellik; PC'de kullanılıyorsa Entegre olarak ayarlayın.",
"gpuAuto": "Otomatik",
"gpuIntegrated": "Entegre",
"gpuDedicated": "Ayrılmış",
@@ -131,6 +131,8 @@
"closeLauncher": "Başlatıcı Davranışı",
"closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat",
"closeOnStartDescription": "Hytale başlatıldıktan sonra başlatıcıyı otomatik olarak kapatın",
"hwAccel": "Donanım Hızlandırma",
"hwAccelDescription": "Başlatıcı için donanım hızlandırmasını etkinleştir",
"gameBranch": "Oyun Dalı",
"branchRelease": "Yayın",
"branchPreRelease": "Ön-Yayın",
@@ -207,7 +209,9 @@
"modsDownloadFailed": "Mod indirilemedi: {error}",
"modsToggleFailed": "Mod değiştirilemedi: {error}",
"modsDeleteFailed": "Mod silinemedi: {error}",
"modsModNotFound": "Mod bilgileri bulunamadı"
"modsModNotFound": "Mod bilgileri bulunamadı",
"hwAccelSaved": "Donanım hızlandırma ayarı kaydedildi",
"hwAccelSaveFailed": "Donanım hızlandırma ayarı kaydedilemedi"
},
"confirm": {
"defaultTitle": "Eylemi onayla",

View File

@@ -333,109 +333,6 @@ body {
pointer-events: auto !important;
}
.discord-notification {
position: fixed;
bottom: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(20px);
border: 1px solid rgba(88, 101, 242, 0.3);
border-radius: 12px;
padding: 1rem;
max-width: 300px;
z-index: 10000;
pointer-events: auto;
display: flex;
align-items: center;
gap: 0.75rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
animation: slideIn 0.5s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification-content {
display: flex;
align-items: center;
gap: 0.75rem;
flex: 1;
}
.discord-notification .fab.fa-discord {
color: #5865f2;
font-size: 1.25rem;
}
.notification-text {
color: white;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.notification-action {
background: #5865f2;
border: none;
border-radius: 6px;
color: white;
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
font-family: 'Space Grotesk', sans-serif;
}
.notification-action:hover {
background: #4752c4;
transform: scale(1.05);
}
.notification-close {
background: none;
border: none;
color: #9ca3af;
cursor: pointer;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
flex-shrink: 0;
}
.notification-close:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.notification-close i {
font-size: 0.75rem;
}
.discord-notification.hidden {
animation: slideOut 0.3s ease-in forwards;
}
@keyframes slideOut {
to {
transform: translateX(100%);
opacity: 0;
}
}
.control-btn {
width: 28px;
height: 28px;
@@ -1107,6 +1004,216 @@ body {
padding-bottom: 1rem;
}
/* Featured Servers Styles */
.featured-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
height: calc(100vh - 180px);
overflow: hidden;
}
.featured-left,
.featured-right {
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.featured-header {
margin-bottom: 1.5rem;
flex-shrink: 0;
}
.featured-title {
font-size: 1.5rem;
font-weight: 700;
color: white;
display: flex;
align-items: center;
gap: 0.5rem;
}
.featured-list {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding-right: 0.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
min-height: 0;
}
.featured-list::-webkit-scrollbar {
width: 8px;
}
.featured-list::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
.featured-list::-webkit-scrollbar-thumb {
background: rgba(147, 51, 234, 0.5);
border-radius: 4px;
}
.featured-list::-webkit-scrollbar-thumb:hover {
background: rgba(147, 51, 234, 0.7);
}
.featured-server-card {
position: relative;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
display: grid;
grid-template-columns: 200px 1fr;
min-height: 120px;
flex-shrink: 0;
}
.featured-server-card:hover {
transform: translateX(4px);
border-color: rgba(147, 51, 234, 0.5);
box-shadow: 0 8px 40px rgba(147, 51, 234, 0.2);
}
.featured-server-banner {
width: 200px;
height: 100%;
min-height: 120px;
object-fit: cover;
background: linear-gradient(135deg, #1e293b, #334155);
flex-shrink: 0;
}
.featured-server-content {
padding: 1.25rem;
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.75rem;
}
.featured-server-name {
font-size: 1.15rem;
font-weight: 600;
color: white;
line-height: 1.4;
margin: 0;
}
.featured-server-address {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.05);
padding: 0.625rem 1rem;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.server-address-text {
font-family: 'JetBrains Mono', monospace;
color: #94a3b8;
font-size: 0.9rem;
}
.copy-address-btn {
background: linear-gradient(135deg, #9333ea, #7c3aed);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
}
.copy-address-btn:hover {
background: linear-gradient(135deg, #7c3aed, #6d28d9);
transform: scale(1.05);
}
.copy-address-btn:active {
transform: scale(0.95);
}
.copy-address-btn.copied {
background: linear-gradient(135deg, #10b981, #059669);
}
.loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: #94a3b8;
gap: 1rem;
}
.loading-spinner i {
color: #9333ea;
}
/* My server card - without banner */
.my-server-card {
position: relative;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
padding: 1.25rem;
flex-shrink: 0;
}
.my-server-card:hover {
transform: translateX(4px);
border-color: rgba(147, 51, 234, 0.5);
box-shadow: 0 8px 40px rgba(147, 51, 234, 0.2);
}
.my-server-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.my-server-name {
font-size: 1.15rem;
font-weight: 600;
color: white;
line-height: 1.4;
margin: 0;
}
.my-server-address {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.05);
padding: 0.625rem 1rem;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.news-view-all:hover {
color: white;
}
@@ -1291,6 +1398,26 @@ body {
max-width: 600px;
margin: 0 auto;
padding-top: 2rem;
padding-bottom: 4rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
@media (min-width: 1024px) {
.settings-content {
max-width: 1000px;
flex-direction: row;
align-items: start;
}
}
.settings-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 1.5rem;
min-width: 0;
}
.setting-group {
@@ -2811,6 +2938,126 @@ body {
border-radius: 4px;
}
.discord-popup-modal {
max-width: 500px;
width: 90%;
}
.discord-popup-header {
display: flex;
align-items: center;
gap: 1rem;
}
.discord-popup-header i {
font-size: 2.5rem;
color: #5865f2;
}
.discord-popup-body {
text-align: center;
padding: 0;
}
.discord-popup-text {
font-size: 1rem;
color: #d1d5db;
margin: 0.5rem 0;
line-height: 1.5;
}
.discord-popup-text strong {
color: #5865f2;
font-weight: 700;
}
.discord-popup-warning {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 8px;
padding: 0.75rem 1rem;
margin: 1.5rem 0;
color: #fbbf24;
font-size: 0.9rem;
}
.discord-popup-warning i {
font-size: 1.1rem;
}
.discord-popup-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
.discord-popup-btn {
flex: 1;
padding: 0.875rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
font-family: 'Space Grotesk', sans-serif;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.discord-popup-btn.primary {
background: #5865f2;
color: white;
box-shadow: 0 4px 0 0 #4752c4, 0 8px 20px rgba(88, 101, 242, 0.3);
}
.discord-popup-btn.primary:hover {
background: #4752c4;
box-shadow: 0 2px 0 0 #4752c4, 0 12px 30px rgba(88, 101, 242, 0.4);
transform: translateY(2px);
}
.discord-popup-btn.primary:active {
transform: translateY(4px);
box-shadow: 0 0px 0 0 #4752c4, 0 4px 15px rgba(88, 101, 242, 0.3);
}
.discord-popup-btn.secondary {
background: rgba(255, 255, 255, 0.1);
color: #d1d5db;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.discord-popup-btn.secondary:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
color: white;
}
.discord-popup-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
box-shadow: none !important;
}
.discord-popup-btn.primary:disabled:hover {
background: #5865f2;
transform: none;
}
.discord-popup-btn.secondary:disabled:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
color: #d1d5db;
}
.mods-grid::-webkit-scrollbar-thumb:hover,
.modal-body::-webkit-scrollbar-thumb:hover {
background: rgba(147, 51, 234, 0.8);
@@ -3795,10 +4042,7 @@ body {
font-weight: 700;
}
.chat-container {
.settings-container {
display: flex;
flex-direction: column;
height: calc(100vh - 180px);
@@ -4242,6 +4486,12 @@ body {
overflow-y: auto;
}
@media (min-width: 1024px) {
.settings-container {
max-width: 1200px;
}
}
.settings-header {
margin-bottom: 1rem;
text-align: center;
@@ -4271,6 +4521,11 @@ body {
transition: all 0.3s ease;
}
.settings-content .settings-section,
.settings-column .settings-section {
margin-bottom: 0;
}
.settings-section:hover {
border-color: rgba(147, 51, 234, 0.3);
box-shadow: 0 8px 32px rgba(147, 51, 234, 0.1);
@@ -5441,8 +5696,8 @@ select.settings-input option {
font-weight: bold;
}
/* Styles pour le sélecteur de couleur dans le chat */
.chat-header-actions {
.settings-container {
padding: 2rem;
display: flex;
align-items: center;
gap: 1rem;

View File

@@ -2,7 +2,7 @@
# Maintainer: Fazri Gading <fazrigading@gmail.com>
# This PKGBUILD is for Github Releases
pkgname=Hytale-F2P
pkgver=2.1.1
pkgver=2.2.0
pkgrel=1
pkgdesc="Hytale-F2P - unofficial Hytale Launcher for free to play with multiplayer support"
arch=('x86_64')
@@ -10,6 +10,8 @@ url="https://github.com/amiayweb/Hytale-F2P"
license=('custom')
depends=('gtk3' 'nss' 'libxcrypt-compat')
makedepends=('npm')
provides=('Hytale-F2P-git' 'hytale-f2p-git')
conflicts=('Hytale-F2P-git' 'hytale-f2p-git')
source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop")
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')

View File

@@ -10,25 +10,25 @@ url="https://github.com/amiayweb/Hytale-F2P"
license=('custom')
depends=('gtk3' 'nss' 'libxcrypt-compat')
makedepends=('git' 'npm')
provides=('Hytale-F2P' 'hytale-f2p-git')
conflicts=('Hytale-F2P' 'hytale-f2p-git')
source=("git+$url.git" "$_pkgname.desktop")
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
pkgver() {
cd "$srcdir/$_pkgname"
cd "$_pkgname"
git describe --tags --long | sed 's/^v//;s/-/.r/;s/-/./'
}
build() {
cd "$srcdir/$_pkgname"
cd "$_pkgname"
npm ci
npm run build:arch
}
package() {
cd "$srcdir/$_pkgname"
install -d "$pkgdir/opt/$_pkgname"
mkdir -p "$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"
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"
}

149
README.md
View File

@@ -4,19 +4,20 @@
<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>
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support!</small></p>
</header>
![Version](https://img.shields.io/badge/Version-2.1.1-green?style=for-the-badge)
![GitHub Downloads](https://img.shields.io/github/downloads/amiayweb/Hytale-F2P/total?style=for-the-badge)
![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-orange?style=for-the-badge)
![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge)
![Version](https://img.shields.io/badge/Version-2.2.0-green?style=for-the-badge)
[![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/network/members)
**If you find this project useful, please give it a STAR!**
### ⚠️ **READ [QUICK START](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-quick-start) before Downloading & Installing the Launcher!** ⚠️
### ⚠️ **READ [QUICK START](README.md#-quick-start) before Downloading & Installing the Launcher!** ⚠️
#### 🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑
@@ -35,37 +36,37 @@
## 📸 Screenshots
<div align="center">
<img src="https://i.imgur.com/xW9do3d.png" alt="Hytale F2P Launcher" width="1000">
<img src="https://i.imgur.com/wwuuMUf.png" alt="Hytale F2P Launcher" width="1000">
<details>
<summary><b>View Gallery</b></summary>
<table style="width: 100%; border-spacing: 15px; border-collapse: separate;">
<tr>
<td align="center" style="vertical-align: top; width: 50%;">
<b>Mods Preview</b><br>
<img src="https://i.imgur.com/f8qyIJy.png" alt="Hytale F2P Mods" width="100%">
<b>Featured Servers 🆕</b><br>
<img src="https://i.imgur.com/fEu9y3Z.png" alt="Hytale F2P Featured Servers" width="100%">
</td>
<td align="center" style="vertical-align: top; width: 50%;">
<b>Latest News</b><br>
<img src="https://i.imgur.com/qu0HltD.png" alt="Hytale F2P News" width="100%">
<b>Settings Page ⚙️</b><br>
<img src="https://i.imgur.com/l5iBzxc.png" alt="Hytale F2P Settings Page" width="100%">
</td>
</tr>
<tr>
<td align="center" style="vertical-align: top; width: 50%;">
<b>Social & Chat</b><br>
<img src="https://i.imgur.com/t3GmbfF.png" alt="Hytale F2P Chat" width="100%">
<b>Downloadable Mods from CurseForge 🛠️</b><br>
<img src="https://i.imgur.com/QIDbqYn.png" alt="Hytale F2P Mods Download" width="100%">
</td>
<td align="center" style="vertical-align: top; width: 50%;">
<b>Settings</b><br>
<img src="https://i.imgur.com/uUD7lDB.png" alt="Hytale F2P Settings" width="100%">
<b>My Mods Menu 🔧</b><br>
<img src="https://i.imgur.com/rjvwUfq.png" alt="Hytale F2P My Mods Menu" width="100%">
</td>
</tr>
<tr>
<td align="center" style="vertical-align: top; width: 50%;">
<b>In-Game Screenshot - Spawn Point</b><br>
<b>In-Game Screenshot - Spawn Point 🎮</b><br>
<img src="https://i.imgur.com/X8lNFQ7.png" alt="Hytale F2P In-Game Screenshot-1" width="100%">
</td>
<td align="center" style="vertical-align: top; width: 50%;">
<b>In-Game Screenshot - Gameplay Terrain</b><br>
<b>In-Game Screenshot - Gameplay Terrain 🌳</b><br>
<img src="https://i.imgur.com/3iRScPa.png" alt="Hytale F2P In-Game Screenshot-2" width="100%">
</td>
</tr>
@@ -79,7 +80,7 @@
🎯 **Core Features**
- 🔄 **Automatic Updates** - Smart version checking and seamless game updates
- 💾 **Data Preservation** - Intelligent UserData backup and restoration during updates
- 🌐 **Cross-Platform** - Full support for Windows, Linux (X11/Wayland), and macOS
- 🌐 **Cross-Platform** - Full support for Windows x64, Linux x64 (X11/Wayland, SteamDeck), and macOS Silicon
-**Java Management** - Automatic Java runtime detection and installation
- 🎮 **Multiplayer Support** - Automatic multiplayer client installation (Windows, macOS & Linux !)
@@ -87,7 +88,6 @@
- 📁 **Custom Installation** - Choose your own installation directory
- 🔍 **Smart Detection** - Automatic game and dependency detection
- 🗂️ **Mod Support** - Built-in mod management system
- 💬 **Player Chat** - Integrated chat system for community interaction
- 📰 **News Feed** - Stay updated with the latest Hytale news
- 🎨 **Modern UI** - Clean, responsive interface with dark theme
@@ -118,9 +118,9 @@
<tr>
<td><b>🖥️ OS</b></td>
<td colspan="3" align="center">
Windows 10/11 (64-bit; X64/ARM64) | Linux (x64/ARM64) | macOS (Apple Silicon only)
Windows 10/11 (64-bit X64) | Linux (x64) | macOS (ARM64/Apple Silicon)
<br />
<small><i>⚠️ Note: macOS Intel (x86) is not yet supported <sup><a href="#fn1" id="ref1">1</a></sup></i></small>
<small><i>⚠️ Note: ARM64 (Windows & Linux), macOS (x86/Intel) <b>are not supported!</b> ⚠️</i></small>
</td>
</tr>
<tr>
@@ -131,7 +131,7 @@
</tr>
<tr>
<td><b>🧠 RAM</b></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>8GB (dGPU) / 12GB (iGPU)<sup><a href="#fn1" id="ref1">1</a></sup></td>
<td>16 GB</td>
<td>32 GB</td>
</tr>
@@ -156,31 +156,28 @@
</tbody>
</table>
</div>
<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>
<p id="fn1"><sup>Note 1</sup> Using Discrete/Dedicated GPU (dGPU) must have 8 GB RAM minimum, while using Integrated GPU (iGPU) must have 12 GB RAM.</p>
> [!WARNING]
> Our launcher has **not yet** supported Offline Mode (playing Hytale without internet).
> We will surely add the feature as soon as possible. Kindly wait for the update.
---
### 🪟 Windows Prequisites
* **
* **Java JDK 25:**
* [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows), **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.
* [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk25-windows)
* [Adoptium](https://adoptium.net/temurin/releases/?version=25)
* [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
> [!WARNING]
> Ubuntu-based Distro like ZorinOS or Pop!_OS or Linux Mint would encounter issues due to UbuntuLTS environment, [check this Discord post](https://discord.com/channels/1462260103951421493/1463662398501027973).
* Make sure you have already installed newest **GPU driver**, consult your distro docs or wiki.
* Install `libpng` package to avoid SDL3_Image error:
* Make sure you have already installed newest **GPU driver** especially proprietary NVIDIA, consult your distro docs or wiki.
* Also make sure that your GPU can be connected to EGL, try checking it first (again, consult your distro docs or wiki) before installing Hytale game via our launcher.
* Install `libpng` package to avoid `SDL3_Image` error:
* `libpng16-16 libpng-dev` for Ubuntu/Debian-based Distro
* `libpng libpng-devel` for Fedora/RHEL-based Distro
* `libpng` for Arch-based Distro
@@ -194,11 +191,12 @@
1. **Prerequisites:** Ensure you have installed all [**Windows Prerequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-windows-prequisites) listed above.
2. **Download:** Get the latest `Hytale-F2P-Launcher.exe` from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page.
3. **SmartScreen Note:** Since the executable is currently unsigned, Windows may show a "Windows protected your PC" popup.
* Click **More info**.
* Click **Run anyway**.
* Click **More info**, then click **Run anyway**.
4. **Launch:** Once installed, you can launch the app directly from your Desktop or the Start menu.
---
5. **Whitelist in Windows Firewall** [#192](https://github.com/amiayweb/Hytale-F2P/issues/192#issuecomment-3803042908)
* Open the Windows Start Menu and search for `Allow an app through Windows Firewall`
* Click "Change settings" (you may need Admin privileges) and Locate `HytaleClient.exe` in the list.
* Ensure both the Private and Public checkboxes are checked. Click OK to save.
### 🐧 Linux Installation
@@ -219,7 +217,7 @@
# Fedora/RHEL-based
sudo dnf install hytale-f2p-launcher.rpm
# Debian/Ubuntu
sudo apt install -y libasound2 libpng16-16 libpng-dev libicu76
sudo apt install -y libasound2 libpng16-16 libpng-dev libicu76 # Not needed in v2.2.0+
sudo dpkg -i hytale-f2p-launcher.deb
```
* **Arch Linux (pacman):** Install the package using:
@@ -238,13 +236,6 @@
> [!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
---
### 🍎 macOS Installation
> [!NOTE]
@@ -259,7 +250,7 @@
* Look for the message regarding "Hytale F2P Launcher" and click **Open Anyway**.
* Authenticate with your password and click **Open**.
#### **Advanced: Manual Installation (.zip)**
#### **Advanced macOS: Manual Installation (.zip)**
The `.zip` version is useful for users who prefer a portable installation or need to bypass specific permission issues.
1. **Extract:** Download and unzip the file to your desired location (e.g., `~/Applications`).
@@ -272,34 +263,41 @@ The `.zip` version is useful for users who prefer a portable installation or nee
---
# How to Host a Server
# 📢 How to Host a Server
## Host your Singleplayer Server (Online-Play Feature)
## 🌐 Host your Singleplayer Server (Online-Play Feature)
> [!NOTE]
> You have to play the game to host the server. See Dedicated Server section below if you want to host it without you playing as the host.
1. Open your Singleplayer World
2. Pause the game (Esc) > select Online Play > Turn on `Allow Other Players to Join` > Set password if needed > Press `Save`.
3. Check the status `Connected via STUN` or `Connected via UPnP`.
3. Check the status `Connected via UPnP`.
4. If your friends can't connect to your hosted Online-Play feature, please follow **Local Dedicated Server** tutorial.
## Dedicated Server
## 🖧 Host a Dedicated Server
> [!NOTE]
> If you have already `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server.
> If you already have the patched `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server. Put the `.bat`/`.sh` script from our Discord server inside your `.../latest/Server` folder.
> [!TIP]
> Use services like Playit.gg, Tailscale, Radmin VPN to share UDP connection if setting up router as an admin is not possible.
> [!WARNING]
> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting).
> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting). Additional: **Linux ARM64** is supported for server only, not client.
> [!IMPORTANT]
> See detailed information of setting up a server here: [SERVER.md](SERVER.md)
> See detailed information of setting up a server here: [SERVER.md](SERVER.md). Download the latest patched JAR, the patched RAR, or the SH/BAT scripts from channel `#open-public-server` in our Discord Server.
---
## 🛠️ Building from Source
## 🔧 Troubleshooting
See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for detailed Troubleshooting guide.
---
## 🔨 Building from Source
See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
@@ -307,15 +305,28 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
## 📋 Changelog
### 🆕 v2.1.1
### 🆕 v2.2.0
- 🔃 **Game Patches Auto-Update Improvement:** No need to install 1.5GB for every updates! Game updates now reduced to almost **~90%** (Hytale Game Update 3 to 4 only take ~150MB).
- 🩹 **Improved Patch System Pre-Release JAR:** In previous version, only Release JAR could be patched. Now it also can be used for Pre-Release JAR!
- 🔗 **Fix Mods Manager Issue:** Mods now can be downloaded seamlessly from the launcher, use Profiles to install your preferred mod. It will also automatically copy from selected `Profile/<profilename>` to the `Mods` folder.
- 💾 **New User Data Location:** UserData Migration to Centralized Location. User data now preserves in `HytaleSaves` located beside `HytaleF2P` folder.
- 🎮 **SteamDeck and Ubuntu/Debian-based Library Fix:** Replace bundled `libzstd.so` with system version to fix `glibc 2.41+` crash.
- 🍎 **Launcher auto-update Improvement for macOS:** Fix auto-install fails on unsigned app. Added option to download the new launcher version on Github website.
- 🌎 **New Translations**: Added France 🇲🇫, German 🇩🇪, Indonesian 🇮🇩, Russia 🇷🇺, and Swedish 🇸🇪 translations to the launcher.
- 🔐 **Fixes Tar Vulnerability:** Updates `tar` from version `6.2.1` to `7.5.7` for vulnerability issue.
- ⚙️ **Improved Settings Pane UI:** Settings are now shown in two columns instead of one. No more doom scrolling just to change your language.
- ⭐ **Added Features Servers:** Don't know which one to play? Join our Featured Servers!
- 💬 **Removed Chat Pane and Add Discord Feature:** Useless chat feature, we got Discord. Join it, NOW. Also added Discord RPC features to Github and our Discord Server. SHOW OFF TO YOUR FRIENDS.
- 🔍 **Investigation on Avatar Not Saving Bug:** We are currently investigating this issue.
<details><summary>Click here to see older Changelogs</summary>
### 🔄 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.1.0
- 🚨 **Auto-Retry Downloads and Auto-Patch Files** —
- ⚡ **Hardware Acceleration** —
@@ -383,6 +394,7 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
- 🎨 **Modern Interface** - Clean, intuitive design
- 🌟 **First Release** - Core launcher functionality
</details>
---
## 👥 Contributors
@@ -396,19 +408,30 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
</div>
### 🏆 Project Creator
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator | Windows*
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator*
- [**@Relyz1993**](https://github.com/Relyz1993) - *Server Helper & Second Developer & Project Creator*
### 🌟 Contributors
### 🌟 Main Contributors
- [**@sanasol**](https://github.com/sanasol) - *Main Issues fixer | Multiplayer Patcher*
- [**@Terromur**](https://github.com/Terromur) - *Main Issues fixer | Beta tester*
- [**@fazrigading**](https://github.com/fazrigading) - *Main Issues fixer | Beta tester*
- [**@fazrigading**](https://github.com/fazrigading) - *Main Issues fixer | Beta tester | Github Release Maintainer*
- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester*
- [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer*
- [**@crimera**](https://github.com/crimera) - *Issues fixer*
- [**@Citeli-py**](https://github.com/Citeli-py) - *Issues fixer*
- [**@Rahul-Sahani04**](https://github.com/Rahul-Sahani04) - *Issues fixer*
- [**@xSamiVS**](https://github.com/xSamiVS) - *Language Translator*
- [**@xSamiVS**](https://github.com/xSamiVS) - *Issues fixer | Language Translator*
#### 🎟️ Fresh Contributors
- [**@GreenKod**](https://github.com/GreenKod) - *Code refractor*
- [**@Citeli-py**](https://github.com/Citeli-py) - *Linux fix & packages version in early release*
- [**@crimera**](https://github.com/crimera) - *Generate new UUID for new username string feature*
- [**@letha11**](https://github.com/letha11) - *CSS filename typo fix*
- [**@colbster937**](https://github.com/colbster937) - *Icon upscaler*
- [**@ArnavSingh77**](https://github.com/ArnavSingh77) - *Close game launcher on start feature, improve app termination behavior*
- [**@TalesAmaral**](https://github.com/TalesAmaral) - *BUILD.md link fix in README.md*
#### 🌐 Language Translators
- [**@BlackSystemCoder**](https://github.com/BlackSystemCoder) - *Russian Language Translator*
- [**@walti0**](https://github.com/walti0) - *Polish Language Translator*
---

351
SERVER.md
View File

@@ -1,34 +1,99 @@
# Hytale F2P Server Guide
# 🎮 Hytale F2P Server Guide
Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup.
DOWNLOAD SERVER FILES HERE: https://discord.gg/MEyWUxt77m
### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/MEyWUxt77m**
**Table of Contents**
* ["Server" Term and Definition](#server-term-and-definiton)
* [Server Directory Location](#server-directory-location)
* [A. Online Play Feature](#a-online-play-feature)
* [1. Host Your Singleplayer World using In-Game Invite Code](#1-host-your-singleplayer-world-using-in-game-invite-code)
* [Common Issues (UPnP/NAT/STUN) on Online Play](#common-issues-upnpnatstun-on-online-play)
* [2. Host Your Singleplayer World using Tailscale](#2-host-your-singleplayer-world-using-tailscale)
* [B. Local Dedicated Server](#b-local-dedicated-server)
* [1. Using Playit.gg (Recommended) ✅](#1-using-playitgg-recommended-)
* [2. Using Radmin VPN](#2-using-radmin-vpn)
* [C. 24/7 Dedicated Server (Advanced)](#c-247-dedicated-server-advanced)
* [Step 1: Get the Files Ready](#step-1-get-the-files-ready)
* [Step 2: Place HytaleServer.jar in the Server directory](#step-2-place-hytaleserverjar-in-the-server-directory)
* [Step 3: Run the Server](#step-3-run-the-server)
* [D. Tinkering Guides](#d-tinkering-guides)
* [1. Network Setup](#1-network-setup)
* [2. Configuration](#2-configuration)
* [3. RAM Allocation Guide](#3-ram-allocation-guide)
* [4. Server Commands](#4-server-commands)
* [5. Command Line Options](#5-command-line-options)
* [6. File Structure](#6-file-structure)
* [7. Backups](#7-backups)
* [8. Troubleshooting](#8-troubleshooting)
* [9. Docker Deployment (Advanced)](#9-docker-deployment-advanced)
* [10. Getting Help](#10-getting-help)
---
### "Server" Term and Definiton
"HytaleServer.jar", which called as "Server", functions as the place of authentication of the client that supposed to go to Hytale Official Authentication System but we managed our way to redirect it on our service (Thanks to Sanasol), handling approximately thousands of players worldwide to play this game for free.
Kindly support us via [our Buy Me a Coffee link](https://buymeacoffee.com/hf2p) if you think our launcher took a big part of developing this Hytale community for the love of the game itself.
**We will always advertise, always pushing, and always asking, to every users of this launcher to purchase the original game to help the official development of Hytale**.
### Server Directory Location
Here are the directory locations of Server folder if you have installed
- **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server`
- **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
- **Linux:** `~/.hytalef2p/release/package/game/latest/Server`
> [!NOTE]
> This location only exists if the user installed the game using our launcher. The `Server` folder needed to auth the HytaleClient to play Hytale online
> (for now; we planned to add offline mode in later version of our launcher).
> [!IMPORTANT]
> Hosting a dedicated Hytale server will not need the exact similar tree. You can put it anywhere, as long as the directory has `Assets.zip` which
> can be acquired from our launcher via our `HytaleServer.rar` server file (which contains patched `HytaleServer.jar`, `Assets.zip`, and `run_server` scripts in `.sh & .bat`.
---
## Part 1: Playing with Friends (Online Play)
# A. Host Your Singleplayer World
This feature is perfect for 1-5 players that want to just play instantly with friends.
Terms and conditions applies.
## 1. Using Online-Play Feature / In-Game Invite Code
The easiest way to play with friends - no manual server setup required!
*The game automatically handles networking using UPnP/STUN/NAT traversal.*
### How It Works
1. **Start the game** via F2P Launcher
2. **Click "Online Play"** in the main menu
3. **Share the invite code** with your friends
4. Friends enter your invite code to join
The game automatically handles networking using UPnP/STUN/NAT traversal.
### Network Requirements
For Online Play to work, you need:
**For Online Play to work, you need:**
- **UPnP enabled** on your router (most routers have this on by default)
- **Public IP address** from your ISP (not behind CGNAT)
### Common Issues
> [!TIP]
> Hoster need to make sure that the router can use UPnP: read router docs, wiki, or watch Youtube tutorials.
>
> If you encounter any problem, check Common Issues section below!
#### "NAT Type: Carrier-Grade NAT (CGNAT)" Warning
1. Press **Worlds** on the Main Menu.
2. Select which world you want to play with your friend.
3. Once you get in the world, press **ESC**/Pause the game.
4. Press **Online Play** in the Pause Menu.
5. Set option "Allow Other Players to Join" from OFF to **ON**. You can set Password if you want.
6. Press **Save**, the Invite Code will appear.
7. Press **Copy to Clipboard** and **Share the Invite Code** to your friends!
8. Friends: Press **Servers** in the Main Menu > Press **Join via Code** > Paste the Code > Join.
> [!WARNING]
> If other players can't join the Hoster with error: `Failed to connect to any available address. The host may be offline or behind a strict firewall.`
>
> **AND ALSO** the Hoster "Online Play" menu shows `Connected to STUN - NAT Type: Restricted (No UPnP)`,
>
> this means the Online Play is **unavailable** on the Hoster machine, and it is neccessary to use services to host your world. **We recommend Playit.gg!**
### Common Issues (UPnP/NAT/STUN) on Online Play
<details><summary><b>a. "NAT Type: Carrier-Grade NAT (CGNAT)" Warning</b></summary>
If you see this message:
```
@@ -40,14 +105,13 @@ Warning: Your network configuration may prevent other players from connecting.
**What this means:** Your ISP doesn't give you a public IP address. Multiple customers share one public IP, which blocks incoming connections.
**Solutions:**
1. **Contact your ISP** - Request a public/static IP address (may cost extra)
2. **Use a VPN with port forwarding** - Services like Mullvad, PIA, or AirVPN offer this
3. **Use Radmin VPN or Playit.gg** - Create a virtual LAN with friends (see below)
3. **Use Playit.gg / Tailscale / Radmin VPN** - Create a virtual LAN with friends (see below)
4. **Have a friend with public IP host instead**
</details>
#### "UPnP Failed" or "Port Mapping Failed"
<details><summary><b>b. "UPnP Failed" or "Port Mapping Failed" Warning</b></summary>
**Check your router:**
1. Log into router admin panel (usually `192.168.1.1` or `192.168.0.1`)
2. Find UPnP settings (often under "Advanced" or "NAT")
@@ -56,117 +120,138 @@ Warning: Your network configuration may prevent other players from connecting.
**If UPnP isn't available:**
- Manually forward **port 5520 UDP** to your computer's local IP
- See "Port Forwarding" section below
#### "Strict NAT" or "Symmetric NAT"
- See "Port Forwarding" or "Workarounds or NAT/CGNAT" sections below
</details>
<details><summary><b>c. "Strict NAT" or "Symmetric NAT" Warning</b></summary>
Some routers have restrictive NAT that blocks peer connections.
**Try:**
1. Enable "NAT Passthrough" or "NAT Filtering: Open" in router settings
2. Put your device in router's DMZ (temporary test only)
3. Use Radmin VPN as workaround
3. Use Playit.gg / Tailscale / Radmin VPN as workaround
</details>
### Workarounds for NAT/CGNAT Issues
## 2. Using Tailscale
Tailscale creates mesh VPN service that streamlines connecting devices and services securely across different networks. And **works crossplatform!!**
#### Option 1: playit.gg (Recommended)
1. All members are required to download [Tailscale](https://tailscale.com/download) on your device.
[Once installed, Tailscale starts and live inside your hidden icon section in Windows, Mac and Linux]
2. Create a **common Tailscale** account which will shared among your friends to log in.
3. Ask your **host to login in to thier Tailscale client first**, then the other members.
* Host
* Open your singleplayer world
* Go to Online Play settings
* Re-save your settings to generate a new share code
* Friends
* Ensure Tailscale is connected
* Use the new share code to connect
* To test your connection, ping the host's ipv4 mentioned in Tailscale
---
# B. Local Dedicated Server
This option is perfect for any players size. From small to high.
## 1. Using Playit.gg (Recommended) ✅
Free tunneling service - only the host needs to install it:
1. **Download [playit.gg](https://playit.gg/)** and run it - Connect your account from the terminal (do not close it when playing on the server)
2. **Add a tunnel** - Select "UDP", tunnel description of "Hytale Server", port count `1`, and local port `5520`
3. **Start the tunnel** - You'll get a public address like `xx-xx.gl.at.ply.gg:5520`
4. **Share the address** - Friends connect directly using this address
1. Go to https://playit.gg/login and **Log In** with your existing account, **Create Account** if you don't have one
2. Press "Add a tunnel" > Select `UDP` > Tunnel description of `Hytale Server` > Port count `1` > and Local Port `5520`
3. Press **Start the tunnel** (or you can just run the Playit.gg.EXE if you already installed it on your machine) - You'll get a public address like `xx-xx.gl.at.ply.gg:5520`
4. Go to https://playit.gg/download : `Installer` (Windows) or `x86-64` (Linux) or follow `Debian Install Script` (Debian-based only)
* Windows: Install the `playit-windows.msi`
* Linux:
* Right-click file > Properties > Turn on 'Executable as a Program' | or `chmod +x playit-linux-amd64` on terminal
* Run by double-clicking the file or `./playit-linux-amd64` via terminal
5. Open the URL/link by `Ctrl+Click` it. If unable, select the URL, then Right-Click to Copy (`Ctrl+Shift+C` for Linux) then Paste the URL into your browser to link it with your created account.
6. **WARNING: Do not close the terminal if you are still playing or hosting the server**
7. Once it done, download the `run_server_with_tokens` script file (`.BAT` for Windows, `.SH` for Linux) from our Discord server > channel `#open-public-server`
8. Put the script file to the `Server` folder in `HytaleF2P` directory (`%localappdata%\HytaleF2P\release\package\game\latest\Server`)
9. Copy the `Assets.zip` from the `%localappdata%\HytaleF2P\release\package\game\latest\` folder to the `Server\` folder. (TIP: You can use Symlink of that file to reduce disk usage!)
10. Double-click the .BAT file to host your server, wait until it shows:
```
===================================================
Hytale Server Booted! [Multiplayer, Fresh Universe]
===================================================
```
11. Connect to the server by go to `Servers` in your game client, press `Add Server`, type `localhost` in the address box, use any name for your server.
12. Send the public address in Step 3 to your friends.
Works with both Online Play and dedicated servers. No software needed for players joining.
#### Option 2: Radmin VPN
## 2. Using Radmin VPN
Creates a virtual LAN - all players need to install it:
1. **Download [Radmin VPN](https://www.radmin-vpn.com/)** - All players install it
2. **Create a network** - One person creates, others join with network name/password
3. **Host via Online Play** - Use your Radmin VPN IP instead
4. **Friends connect** - They'll see you on the virtual LAN
1. Download [Radmin VPN](https://www.radmin-vpn.com/) - All players install it
2. One person create a room/network, others join with network name/password
3. Host joined the world, others will connect to it.
4. Open Hytale Game > Servers > Add Servers > Direct Connect > Type IP Address of the Host from Radmin.
Both options bypass all NAT/CGNAT issues. But for **Windows machines only!**
These options bypass all NAT/CGNAT issues. But for **Windows machines only!**
#### Option 3: Tailscale
It creates mesh VPN service that streamlines connecting devices and services securely across different networks. And **works crossplatform!!**
1. All member's are required to download [Tailscale](https://tailscale.com/download) on your device.
[Once installed, Tailwind starts and live inside your hidden icon section in Windows, Mac and Linux]
2. Create a **common tailscale** account which will shared among your friends to log in.
3. Ask your **host to login in to thier tailscale client first**, then the other members.
##### Host
1. Open your singleplayer world
2. Go to Online Play settings
3. Re-save your settings to generate a new share code
##### Friends
1. Ensure Tailscale is connected
2. Use the new share code to connect
[To test your connection, ping the host's ipv4 mentioned in tailwind]
---
## Part 2: Dedicated Server (Advanced)
# C. 24/7 Dedicated Server (Advanced)
For 24/7 servers, custom configurations, or hosting on a VPS/dedicated machine.
### Quick Start
## Step 1: Get the Files Ready
#### Step 1: Get the Server JAR
### Prequisites
The server scripts will automatically download the pre-patched server JAR if it's not present.
1. `HytaleServer.jar` (pre-patched for F2P players; dual-auth soon for Official + F2P play)
2. `Assets.zip`
3. `run_scripts_with_token.bat` for Windows or `run_scripts_with_token.sh` for macOS/Linux
**Option A:** Let the scripts download automatically (requires `HYTALE_SERVER_URL` to be configured)
> [!NOTE]
> The `HytaleServer.rar` available on our Discord Server (`#open-public-server` channel; typo on the Discord, not `zip`) includes all of the prequisites.
> Unfortunately, the JAR inside the RAR isn't updated so you'll need to download the JAR from the link on Discord.
**Option B:** Manually place `HytaleServer.jar` (pre-patched for F2P) in the Server directory:
> [!TIP]
> You can copy `Assets.zip` generated from the launcher to be used for the dedicated server. It's located in `HytaleF2P/release/package/game/latest`.
- **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server`
- **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
- **Linux:** `~/.hytalef2p/release/package/game/latest/Server`
If you have a custom install path, the Server folder is inside your custom location under `HytaleF2P/release/package/game/latest/Server`.
## Step 2: Place `HytaleServer.jar` in the `Server` directory
#### Step 2: Run the Server
* Windows
* `%localappdata%\HytaleF2P\release\package\game\latest\Server`
* macOS
* `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
* Linux
* `~/.hytalef2p/release/package/game/latest/Server`
* If you have a custom install path, the Server folder is inside your custom location under
* `HytaleF2P/release/package/game/latest/Server`.
## Step 3: Run the Server
**Windows:**
```batch
cd scripts
run_server.bat
```
**macOS / Linux:**
```bash
cd scripts
./run_server.sh
```
The scripts will:
1. Find your game installation automatically
2. Download the pre-patched server JAR if needed
3. Fetch session tokens from the auth server
4. Start the server
#### Step 3: Connect Players
Share your server IP address with players. They connect via the F2P Launcher's server browser or direct connect.
---
## Network Setup (Dedicated Server)
# D. Tinkering Guides
### Local Network (LAN)
## 1. Network Setup
### a. Local Network (LAN)
If all players are on the same network:
1. Find your local IP: `ipconfig` (Windows) or `ifconfig` (Mac/Linux)
2. Share this IP with players on your network
3. Default port is `5520`
### Port Forwarding (Internet Play)
### b. Port Forwarding (Internet Play)
To allow direct internet connections:
1. Forward **port 5520 (UDP)** in your router
2. Find your public IP at [whatismyip.com](https://whatismyip.com)
3. Share your public IP with players
@@ -179,36 +264,35 @@ netsh advfirewall firewall add rule name="Hytale Server" dir=in action=allow pro
---
## Configuration
## 2. Configuration
### Environment Variables
### a. Environment Variables
Set these before running to customize your server:
Write this in the script file `.BAT`/`.SH` or set these manually in command before running to customize your server:
| Variable | Default | Description |
|----------|---------|-------------|
| `HYTALE_SERVER_URL` | (placeholder) | URL to download pre-patched server JAR |
| `HYTALE_AUTH_DOMAIN` | `auth.sanasol.ws` | Auth server domain (4-16 chars) |
| `HYTALE_BIND` | `0.0.0.0:5520` | Server IP and port |
| `HYTALE_AUTH_MODE` | `authenticated` | Auth mode (see below) |
| `HYTALE_SERVER_NAME` | `My Hytale Server` | Server display name |
| `HYTALE_GAME_PATH` | (auto-detected) | Override game location |
| `BIND_ADRESS` | `0.0.0.0:5520` | Server IP and port |
| `AUTH_MODE` | `authenticated` | Auth mode (see below) |
| `SERVER_NAME` | `My Hytale Server` | Server display name |
| `ASSETS_PATH` | `./Assets.zip` | Assets file location |
| `JVM_XMS` | `2G` | Minimum Java memory |
| `JVM_XMX` | `4G` | Maximum Java memory |
**Example (Windows):**
```batch
set HYTALE_SERVER_NAME=My Awesome Server
set SERVER_NAME=My Awesome Server
set JVM_XMX=8G
run_server.bat
```
**Example (Linux/macOS):**
```bash
HYTALE_SERVER_NAME="My Awesome Server" JVM_XMX=8G ./run_server.sh
SERVER_NAME="My Awesome Server" JVM_XMX=8G ./run_server.sh
```
### Authentication Modes
### b. Authentication Modes
| Mode | Description | Use Case |
|------|-------------|----------|
@@ -218,7 +302,7 @@ HYTALE_SERVER_NAME="My Awesome Server" JVM_XMX=8G ./run_server.sh
---
## RAM Allocation Guide
## 3. RAM Allocation Guide
Adjust memory based on your system:
@@ -242,7 +326,7 @@ JVM_XMS=4G JVM_XMX=12G ./run_server.sh
---
## Server Commands
## 4. Server Commands
Once running, use these commands in the console:
@@ -259,9 +343,12 @@ Once running, use these commands in the console:
| `unban <player>` | Unban a player |
| `tp <player> <x> <y> <z>` | Teleport player |
Use `/` slash for these commands.
---
## Command Line Options
## 5. Command Line Options
Pass these when starting the server:
@@ -290,7 +377,7 @@ Pass these when starting the server:
---
## File Structure
## 6. File Structure
```
<game_path>/
@@ -308,21 +395,21 @@ Pass these when starting the server:
---
## Backups
## 7. Backups
### Automatic Backups
### a. Automatic Backups
```bash
./run_server.sh --backup --backup-dir ./backups --backup-frequency 30
```
### Manual Backup
### b. Manual Backup
1. Use `save` command or stop the server
2. Copy the `universe/` folder
3. Store in a safe location
### Restore
### c. Restore
1. Stop the server
2. Delete/rename current `universe/`
@@ -331,9 +418,9 @@ Pass these when starting the server:
---
## Troubleshooting
## 8. Troubleshooting
### "Java not found" or "Java version too old"
### a. "Java not found" or "Java version too old"
**Java 21 is REQUIRED** (the server uses class file version 65.0).
@@ -352,30 +439,20 @@ export PATH="$JAVA_HOME/bin:$PATH"
```
Add these lines to `~/.zshrc` or `~/.bash_profile` to make permanent.
### "Game directory not found"
- Download game via F2P Launcher first
- Or set `HYTALE_GAME_PATH` environment variable
- Check custom install path in launcher settings
### "Assets.zip not found"
Game files incomplete. Re-download via the launcher.
### "Port already in use"
### b. "Port already in use"
```bash
./run_server.sh --bind 0.0.0.0:5521
```
### "Out of memory"
### c. "Out of memory"
Increase JVM_XMX:
```bash
JVM_XMX=6G ./run_server.sh
```
### Players can't connect
### d. Players can't connect
1. Server shows "Server Ready"?
2. Using F2P Launcher (not official)?
@@ -383,7 +460,7 @@ JVM_XMX=6G ./run_server.sh
4. Port forwarding configured (for internet)?
5. Try `--auth-mode unauthenticated` for testing
### "Authentication failed"
### e. "Authentication failed"
- Ensure players use F2P Launcher
- Auth server may be temporarily down
@@ -391,7 +468,7 @@ JVM_XMX=6G ./run_server.sh
---
## Docker Deployment (Advanced)
## 9. Docker Deployment (Advanced)
For production servers, use Docker:
@@ -410,40 +487,7 @@ See [Docker documentation](https://github.com/Hybrowse/hytale-server-docker) for
---
## Server Settings Summary
### Minimal Setup
```bash
./run_server.sh
```
### Custom Memory
```bash
JVM_XMS=2G JVM_XMX=8G ./run_server.sh
```
### Custom Port
```bash
HYTALE_BIND=0.0.0.0:25565 ./run_server.sh
```
### LAN Party (No Auth)
```bash
./run_server.sh --auth-mode unauthenticated
```
### Full Custom Setup
```bash
HYTALE_SERVER_NAME="Epic Server" \
HYTALE_BIND=0.0.0.0:5520 \
JVM_XMS=2G \
JVM_XMX=8G \
./run_server.sh --backup --backup-frequency 15 --allow-op
```
---
## Getting Help
## 10. Getting Help
- Check server console logs for errors
- Test with `--auth-mode unauthenticated` first
@@ -452,8 +496,9 @@ JVM_XMX=8G \
---
## Credits
# Credits
- Hytale F2P Project
- [Hybrowse Docker Image](https://github.com/Hybrowse/hytale-server-docker)
- Auth Server: sanasol.ws

460
TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,460 @@
# Hytale F2P Launcher - Troubleshooting Guide
This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [Discord](https://discord.gg/gME8rUy3MB).
---
## Table of Contents
- [Windows Issues](#windows-issues)
- [Linux Issues](#linux-issues)
- [macOS Issues](#macos-issues)
- [Connection & Server Issues](#connection--server-issues)
- [Authentication & Token Issues](#authentication--token-issues)
- [Avatar & Cosmetics Issues](#avatar--cosmetics-issues)
- [General Issues](#general-issues)
- [Known Limitations](#known-limitations)
---
## Windows Issues
### "Failed to connect to server" / Server won't boot
**Symptoms:** Singleplayer world fails to load, "Server failed to boot" error.
**Solution - Whitelist in Windows Firewall:**
1. Open **Windows Settings** > **Privacy & Security** > **Windows Security**
2. Click **Firewall & network protection** > **Allow an app through firewall**
3. Click **Change settings**
4. Find `HytaleClient.exe` and check both **Private** and **Public**
5. If not listed, click **Allow another app** and browse to:
```
%localappdata%\HytaleF2P\release\package\game\latest\Client\HytaleClient.exe
```
### Duplicate mod error
**Symptoms:** `java.lang.IllegalArgumentException: Tried to load duplicate plugin`
**Solution:**
1. Navigate to your mods folder:
```
%localappdata%\HytaleF2P\release\package\game\latest\Client\UserData\Mods
```
2. Remove any duplicate `.jar` files
3. Restart the launcher
### SmartScreen blocks the launcher
**Solution:**
1. Click **More info**
2. Click **Run anyway**
---
## Linux Issues
### GPU not detected / Using software rendering (llvmpipe)
**Symptoms:**
- Game uses integrated GPU instead of dedicated GPU
- Very low FPS / unplayable performance
- Play button not clickable
- Log shows `llvmpipe` instead of your GPU
**Solution for NVIDIA:**
```bash
__EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json ./HytaleF2P.AppImage
```
**Solution for AMD (hybrid GPU systems):**
```bash
DRI_PRIME=1 ./HytaleF2P.AppImage
```
**Permanent fix - Create a launcher script:**
```bash
#!/bin/bash
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/10_nvidia.json
export DRI_PRIME=1
./HytaleF2P.AppImage
```
**Note:** On some desktop systems with AMD iGPU + dGPU, the GPU selector may be inverted (selecting iGPU actually uses dGPU). Use whichever option works.
### SDL3_image / libpng errors
**Symptoms:**
- `DllNotFoundException: Unable to load shared library 'SDL3_image'`
- `libpng` related errors
- Game crashes on startup
**Solution - Install dependencies:**
**Fedora / RHEL:**
```bash
sudo dnf install libpng libpng-devel
```
**Debian / Ubuntu:**
```bash
sudo apt install libpng16-16 libpng-dev libgdiplus libc6-dev
```
**Arch Linux:**
```bash
sudo pacman -S libpng
```
**Alternative - Replace corrupted library:**
```bash
cd ~/.hytalef2p/release/package/game/latest/Client/
mv libSDL3_image.so libSDL3_image.so.bak
wget https://github.com/user-attachments/files/24710966/libSDL3_image.zip
unzip libSDL3_image.zip
chmod 644 libSDL3_image.so
rm libSDL3_image.zip
```
### AppImage won't launch / FUSE error
**Solution:**
```bash
# Debian/Ubuntu
sudo apt install libfuse2
# Fedora
sudo dnf install fuse
# Arch
sudo pacman -S fuse2
```
### Missing libxcrypt.so.1
**Solution:**
```bash
# Fedora/RHEL
sudo dnf install libxcrypt-compat
# Arch
sudo pacman -S libxcrypt-compat
```
### Wayland display issues
**Symptoms:** Game doesn't launch, stuck at loading, or display glitches on Wayland.
**Solution - Force X11:**
```bash
GDK_BACKEND=x11 ./HytaleF2P.AppImage
```
**Alternative - Electron Wayland hint:**
```bash
ELECTRON_OZONE_PLATFORM_HINT=auto ./HytaleF2P.AppImage
```
### Steam Deck / Gamescope issues
**Solution 1 - Add custom launch options in Steam:**
```
ELECTRON_OZONE_PLATFORM_HINT=x11 %command%
```
**Solution 2 - Launch from Desktop Mode** instead of Game Mode.
**Solution 3 - Force X11:**
```bash
GDK_BACKEND=x11 ./HytaleF2P.AppImage
```
### Ubuntu LTS-based distros (Linux Mint, Zorin OS, Pop!_OS)
These distributions may have compatibility issues due to older system packages. This is a limitation of the Hytale game client, not the launcher.
**Workarounds:**
1. Install all dependencies listed above
2. Try the SDL3_image replacement
3. Consider using a more recent distribution or Flatpak/AppImage with bundled dependencies
---
## macOS Issues
### "Butler system error -86" (Apple Silicon)
**Symptoms:** `Butler execution failed: spawn Unknown system error -86` (EXC_BAD_CPU_TYPE)
**Cause:** Butler (the update tool) is x86_64 only.
**Solution - Install Rosetta 2:**
```bash
softwareupdate --install-rosetta
```
Then restart the launcher.
### Auto-update fails with code signature error
**Symptoms:**
```
Code signature at URL did not pass validation
domain: 'SQRLCodeSignatureErrorDomain'
```
**Solution - Manual update:**
1. Download the latest version manually from [Releases](https://github.com/amiayweb/Hytale-F2P/releases/latest)
2. Backup your data first (see [Backup Locations](#backup-locations))
3. Install the fresh download
### "Unidentified developer" warning
**Solution:**
1. Open **System Settings** > **Privacy & Security**
2. Scroll to **Security** section
3. Find the message about "Hytale F2P Launcher"
4. Click **Open Anyway**
5. Authenticate and click **Open**
### App won't open (quarantine)
**Solution:**
```bash
xattr -rd com.apple.quarantine /Applications/Hytale-F2P-Launcher.app
```
---
## Connection & Server Issues
### "Failed to connect to server" in Singleplayer
**Possible causes:**
1. Windows Firewall blocking (see [Windows section](#failed-to-connect-to-server--server-wont-boot))
2. Patched server JAR download failed
3. Regional network restrictions
**Solution - Check patched JAR:**
1. Look for `HytaleServer.jar` in:
- Windows: `%localappdata%\HytaleF2P\release\package\game\latest\Server\`
- Linux: `~/.hytalef2p/release/package/game/latest/Server/`
- macOS: `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server/`
2. If missing or very small, the download may have failed
**Solution - Regional restrictions:**
Some countries (Russia, Turkey, Indonesia, etc.) may have issues accessing download servers.
- Try using a VPN for the initial download
- Once downloaded, the patched JAR is cached locally
### "Infinite Booting Server" / Server stuck loading
**Solution:**
1. Check if the patched JAR downloaded successfully (see above)
2. Ensure your GPU meets minimum requirements
3. Check launcher logs for specific errors
4. Try with a VPN if in a restricted region
### "Connection timed out from inactivity"
**This is expected behavior.** Sessions have a 10-hour TTL and will timeout after extended inactivity. Simply reconnect to continue playing.
---
## Authentication & Token Issues
### "Invalid identity token" / "Failed to start Hytale"
**Solution:**
1. **Restart the launcher** - This fetches fresh tokens
2. **Check system time** - JWT validation requires accurate system time
3. **Clear cached tokens:**
- Delete `config.json` from your HytaleF2P folder
- Restart the launcher
- Re-enter your username
**Locations:**
- Windows: `%localappdata%\HytaleF2P\config.json`
- Linux: `~/.hytalef2p/config.json`
- macOS: `~/Library/Application Support/HytaleF2P/config.json`
### Token refresh errors
If you see issuer mismatch errors in logs:
1. Delete `config.json` and `player_id.json`
2. Restart the launcher
3. This forces a fresh authentication
---
## Avatar & Cosmetics Issues
### Avatar/skin changes not saving
**This is a known F2P limitation:**
- F2P mode has no password protection for usernames
- Anyone can use any username
- Cosmetics are stored server-side by username
- If someone else uses your username, they can change your cosmetics
**Workaround:** Use a unique username that others are unlikely to choose.
### Character invisible / Customization crashes
**Solution:**
1. Use **Repair Game** in launcher Settings
2. Verify `Assets.zip` exists in your game folder
3. Clear cached assets:
- Windows: Delete `%localappdata%\HytaleF2P\release\package\game\latest\Client\UserData\CachedAssets\`
4. Restart the launcher
### Avatar creator shows "Offline Mode"
**Cause:** Cannot connect to auth server.
**Solution:**
1. Check your internet connection
2. Test connectivity: Open `https://auth.sanasol.ws/health` in browser (should show "OK")
3. Check if firewall is blocking the connection
4. Try disabling VPN (or enabling one if in restricted region)
---
## General Issues
### Mods not showing up
**Solution:**
1. Ensure mods are placed in the correct folder:
- Windows: `%localappdata%\HytaleF2P\release\package\game\latest\Client\UserData\Mods\`
- Linux: `~/.hytalef2p/release/package/game/latest/Client/UserData/Mods/`
- macOS: `~/Library/Application Support/HytaleF2P/release/package/game/latest/Client/UserData/Mods/`
2. Verify mod files are `.jar` format
3. Check launcher logs for mod loading errors
### Game updates delete configurations/mods
**This is a known issue being worked on.**
**Prevention - Always backup before updating:**
- Server configs and worlds
- Mods folder
- `config.json` and `player_id.json`
See [Backup Locations](#backup-locations) below.
### Play button not clickable
Usually caused by GPU detection failure. See [GPU not detected](#gpu-not-detected--using-software-rendering-llvmpipe).
**Alternative:**
1. Go to **Settings** > **Graphics**
2. Manually select your GPU
3. Restart the launcher
### Read timeout errors
**Cause:** Network connectivity issues.
**Solutions:**
1. Check your internet connection stability
2. Try using a VPN
3. Check firewall settings
4. Try at a different time (server load varies)
---
## Known Limitations
### Linux ARM64 not supported
Hytale does not provide ARM64 game client builds. The launcher downloads from official Hytale servers which only provide:
- Windows x64
- macOS (Universal/Intel)
- Linux x64
This is outside our control.
### F2P Username System
- No password protection for usernames
- Anyone can claim any username
- Cosmetics shared by username
- UUIDs generated based on username
A per-player password system is planned for future versions.
### Session Timeout
Game sessions have a 10-hour TTL. This is by design for security.
---
## Backup Locations
### Windows
```
%localappdata%\HytaleF2P\
├── config.json # Launcher settings
├── player_id.json # Player identity
└── release\package\game\latest\
├── Client\UserData\ # Saves, settings, mods
└── Server\
├── universe\ # World data
└── config.json # Server config
```
### Linux
```
~/.hytalef2p/
├── config.json
├── player_id.json
└── release/package/game/latest/
├── Client/UserData/
└── Server/
├── universe/
└── config.json
```
### macOS
```
~/Library/Application Support/HytaleF2P/
├── config.json
├── player_id.json
└── release/package/game/latest/
├── Client/UserData/
└── Server/
├── universe/
└── config.json
```
---
## Getting Help
If your issue isn't resolved by this guide:
1. **Check existing issues:** [GitHub Issues](https://github.com/amiayweb/Hytale-F2P/issues)
2. **Join Discord:** [discord.gg/gME8rUy3MB](https://discord.gg/gME8rUy3MB)
3. **Open a new issue** with:
- Your operating system and version
- Launcher version
- Full launcher logs from:
- Windows: `%localappdata%\HytaleF2P\logs\`
- Linux: `~/.hytalef2p/logs/`
- macOS: `~/Library/Application Support/HytaleF2P/logs/`
- Steps to reproduce the issue
---
## Logs Location
For bug reports, please include logs from:
| OS | Path |
|----|------|
| Windows | `%localappdata%\HytaleF2P\logs\` |
| Linux | `~/.hytalef2p/logs/` |
| macOS | `~/Library/Application Support/HytaleF2P/logs/` |

View File

@@ -84,15 +84,6 @@ function loadUsername() {
return config.username || 'Player';
}
function saveChatUsername(chatUsername) {
saveConfig({ chatUsername: chatUsername || '' });
}
function loadChatUsername() {
const config = loadConfig();
return config.chatUsername || '';
}
function getUuidForUser(username) {
const { v4: uuidv4 } = require('uuid');
const config = loadConfig();
@@ -294,17 +285,6 @@ function resetCurrentUserUuid() {
return setUuidForUser(username, newUuid);
}
function saveChatColor(color) {
const config = loadConfig();
config.chatColor = color;
saveConfig(config);
}
function loadChatColor() {
const config = loadConfig();
return config.chatColor || '#3498db';
}
function saveGpuPreference(gpuPreference) {
saveConfig({ gpuPreference: gpuPreference || 'auto' });
}
@@ -343,10 +323,6 @@ module.exports = {
saveConfig,
saveUsername,
loadUsername,
saveChatUsername,
loadChatUsername,
saveChatColor,
loadChatColor,
getUuidForUser,
saveJavaPath,
loadJavaPath,

View File

@@ -15,7 +15,7 @@ function getAppDir() {
}
/**
* Get centralized UserData saves directory (NEW in 2.1.2)
* Get centralized UserData saves directory (NEW in 2.2.0)
* UserData is now stored separately from game installation
*/
function getHytaleSavesDir() {
@@ -233,7 +233,7 @@ async function getModsPath(customInstallPath = null) {
function getProfilesDir(customInstallPath = null) {
try {
// NEW 2.1.2: Use centralized UserData location
// NEW 2.2.0: Use centralized UserData location
const userDataPath = getHytaleSavesDir();
const profilesDir = path.join(userDataPath, 'Profiles');

View File

@@ -0,0 +1,7 @@
const FORCE_CLEAN_INSTALL_VERSION = false;
const CLEAN_INSTALL_TEST_VERSION = '4.pwr';
module.exports = {
FORCE_CLEAN_INSTALL_VERSION,
CLEAN_INSTALL_TEST_VERSION
};

View File

@@ -5,10 +5,6 @@
const {
saveUsername,
loadUsername,
saveChatUsername,
loadChatUsername,
saveChatColor,
loadChatColor,
saveJavaPath,
loadJavaPath,
saveInstallPath,
@@ -20,10 +16,11 @@ const {
saveCloseLauncherOnStart,
loadCloseLauncherOnStart,
// Hardware Acceleration
saveLauncherHardwareAcceleration,
loadLauncherHardwareAcceleration,
loadConfig,
saveConfig,
saveModsToConfig,
loadModsFromConfig,
@@ -113,10 +110,6 @@ module.exports = {
// User configuration functions
saveUsername,
loadUsername,
saveChatUsername,
loadChatUsername,
saveChatColor,
loadChatColor,
getUuidForUser,
// Java configuration functions
@@ -144,6 +137,10 @@ module.exports = {
saveLauncherHardwareAcceleration,
loadLauncherHardwareAcceleration,
// Config functions
loadConfig,
saveConfig,
// GPU Preference functions
saveGpuPreference,
loadGpuPreference,

View File

@@ -0,0 +1,272 @@
const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
const { downloadFile, retryDownload } = require('../utils/fileManager');
const { getOS, getArch } = require('../utils/platformUtils');
const { validateChecksum, extractVersionDetails, canUseDifferentialUpdate, needsIntermediatePatches, getInstalledClientVersion } = require('../services/versionManager');
const { installButler } = require('./butlerManager');
const { GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
const { saveVersionClient } = require('../core/config');
async function acquireGameArchive(downloadUrl, targetPath, checksum, progressCallback, allowRetry = true) {
const osName = getOS();
const arch = getArch();
if (osName === 'darwin' && arch === 'amd64') {
throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.');
}
if (fs.existsSync(targetPath)) {
const stats = fs.statSync(targetPath);
if (stats.size > 1024 * 1024) {
const isValid = await validateChecksum(targetPath, checksum);
if (isValid) {
console.log(`Valid archive found in cache: ${targetPath}`);
return targetPath;
}
console.log('Cached archive checksum mismatch, re-downloading');
fs.unlinkSync(targetPath);
}
}
console.log(`Downloading game archive from: ${downloadUrl}`);
try {
if (allowRetry) {
await retryDownload(downloadUrl, targetPath, progressCallback);
} else {
await downloadFile(downloadUrl, targetPath, progressCallback);
}
} catch (error) {
const enhancedError = new Error(`Archive download failed: ${error.message}`);
enhancedError.originalError = error;
enhancedError.downloadUrl = downloadUrl;
enhancedError.targetPath = targetPath;
throw enhancedError;
}
const stats = fs.statSync(targetPath);
console.log(`Archive downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
const isValid = await validateChecksum(targetPath, checksum);
if (!isValid) {
console.log('Downloaded archive checksum validation failed, removing corrupted file');
fs.unlinkSync(targetPath);
throw new Error('Downloaded archive is corrupted or invalid. Please retry');
}
console.log(`Archive validation passed: ${targetPath}`);
return targetPath;
}
async function deployGameArchive(archivePath, destinationDir, toolsDir, progressCallback, isDifferential = false) {
if (!archivePath || !fs.existsSync(archivePath)) {
throw new Error(`Archive not found: ${archivePath || 'undefined'}`);
}
const stats = fs.statSync(archivePath);
console.log(`Deploying archive: ${archivePath}`);
console.log(`Archive size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
console.log(`Deployment mode: ${isDifferential ? 'differential' : 'full'}`);
const butlerPath = await installButler(toolsDir);
const stagingDir = path.join(destinationDir, 'staging-temp');
if (!fs.existsSync(destinationDir)) {
fs.mkdirSync(destinationDir, { recursive: true });
}
if (fs.existsSync(stagingDir)) {
fs.rmSync(stagingDir, { recursive: true, force: true });
}
fs.mkdirSync(stagingDir, { recursive: true });
if (progressCallback) {
progressCallback(isDifferential ? 'Applying differential update...' : 'Installing game files...', null, null, null, null);
}
const args = [
'apply',
'--staging-dir',
stagingDir,
archivePath,
destinationDir
];
console.log(`Executing deployment: ${butlerPath} ${args.join(' ')}`);
return new Promise((resolve, reject) => {
const child = execFile(butlerPath, args, {
maxBuffer: 1024 * 1024 * 10,
timeout: 600000
}, (error, stdout, stderr) => {
if (error) {
const cleanStderr = stderr.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
const cleanStdout = stdout.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
if (cleanStderr) console.error('Deployment stderr:', cleanStderr);
if (cleanStdout) console.error('Deployment stdout:', cleanStdout);
const errorText = (stderr + ' ' + error.message).toLowerCase();
let message = 'Game deployment failed';
if (errorText.includes('unexpected eof')) {
message = 'Corrupted archive detected. Please retry download.';
if (fs.existsSync(archivePath)) {
fs.unlinkSync(archivePath);
}
} else if (errorText.includes('permission denied')) {
message = 'Permission denied. Check file permissions and try again.';
} else if (errorText.includes('no space left') || errorText.includes('device full')) {
message = 'Insufficient disk space. Free up space and try again.';
}
const deployError = new Error(message);
deployError.originalError = error;
deployError.stderr = cleanStderr;
deployError.stdout = cleanStdout;
return reject(deployError);
}
console.log('Game deployment completed successfully');
const cleanOutput = stdout.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
if (cleanOutput) {
console.log(cleanOutput);
}
if (fs.existsSync(stagingDir)) {
try {
fs.rmSync(stagingDir, { recursive: true, force: true });
} catch (cleanupErr) {
console.warn('Failed to cleanup staging directory:', cleanupErr.message);
}
}
resolve();
});
child.on('error', (err) => {
console.error('Deployment process error:', err);
reject(new Error(`Failed to execute deployment tool: ${err.message}`));
});
});
}
async function performIntelligentUpdate(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) {
console.log(`Initiating intelligent update to version ${targetVersion}`);
const currentVersion = getInstalledClientVersion();
console.log(`Current version: ${currentVersion || 'none (clean install)'}`);
console.log(`Target version: ${targetVersion}`);
console.log(`Branch: ${branch}`);
if (branch !== 'release') {
console.log(`Pre-release branch detected - forcing full archive download`);
const versionDetails = await extractVersionDetails(targetVersion, branch);
const archiveName = path.basename(versionDetails.fullUrl);
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
if (progressCallback) {
progressCallback('Downloading full game archive (pre-release)...', 0, null, null, null);
}
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
saveVersionClient(targetVersion);
console.log(`Pre-release installation completed. Version ${targetVersion} is now installed.`);
return;
}
if (!currentVersion) {
console.log('No existing installation detected - downloading full archive');
const versionDetails = await extractVersionDetails(targetVersion, branch);
const archiveName = path.basename(versionDetails.fullUrl);
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
if (progressCallback) {
progressCallback(`Downloading full game archive (first install - v${targetVersion})...`, 0, null, null, null);
}
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
saveVersionClient(targetVersion);
console.log(`Initial installation completed. Version ${targetVersion} is now installed.`);
return;
}
const patchesToApply = needsIntermediatePatches(currentVersion, targetVersion);
if (patchesToApply.length === 0) {
console.log('Already at target version or invalid version sequence');
return;
}
console.log(`Applying ${patchesToApply.length} differential patch(es): ${patchesToApply.join(' -> ')}`);
for (let i = 0; i < patchesToApply.length; i++) {
const patchVersion = patchesToApply[i];
const versionDetails = await extractVersionDetails(patchVersion, branch);
const canDifferential = canUseDifferentialUpdate(getInstalledClientVersion(), versionDetails);
if (!canDifferential || !versionDetails.differentialUrl) {
console.log(`WARNING: Differential patch not available for ${patchVersion}, using full archive`);
const archiveName = path.basename(versionDetails.fullUrl);
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
if (progressCallback) {
progressCallback(`Downloading full archive for ${patchVersion} (${i + 1}/${patchesToApply.length})...`, 0, null, null, null);
}
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
} else {
console.log(`Applying differential patch: ${versionDetails.sourceVersion} -> ${patchVersion}`);
const archiveName = path.basename(versionDetails.differentialUrl);
const archivePath = path.join(cacheDir, `${branch}_patch_${archiveName}`);
if (progressCallback) {
progressCallback(`Applying patch ${i + 1}/${patchesToApply.length}: ${patchVersion}...`, 0, null, null, null);
}
await acquireGameArchive(versionDetails.differentialUrl, archivePath, versionDetails.checksum, progressCallback);
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, true);
if (fs.existsSync(archivePath)) {
try {
fs.unlinkSync(archivePath);
console.log(`Cleaned up patch file: ${archiveName}`);
} catch (cleanupErr) {
console.warn(`Failed to cleanup patch file: ${cleanupErr.message}`);
}
}
}
saveVersionClient(patchVersion);
console.log(`Patch ${patchVersion} applied successfully (${i + 1}/${patchesToApply.length})`);
}
console.log(`Update completed successfully. Version ${targetVersion} is now installed.`);
}
async function ensureGameInstalled(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) {
const { findClientPath } = require('../core/paths');
const clientPath = findClientPath(gameDir);
if (clientPath) {
const currentVersion = getInstalledClientVersion();
if (currentVersion === targetVersion) {
console.log(`Game already installed at correct version: ${targetVersion}`);
return;
}
}
await performIntelligentUpdate(targetVersion, branch, progressCallback, gameDir, cacheDir, toolsDir);
}
module.exports = {
acquireGameArchive,
deployGameArchive,
performIntelligentUpdate,
ensureGameInstalled
};

View File

@@ -10,9 +10,11 @@ const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platf
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain, loadVersionBranch, loadVersionClient, saveVersionClient } = require('../core/config');
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
const { getLatestClientVersion } = require('../services/versionManager');
const { updateGameFiles } = require('./gameManager');
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
const { ensureGameInstalled } = require('./differentialUpdateManager');
const { syncModsForCurrentProfile } = require('./modManager');
const { getUserDataPath } = require('../utils/userDataMigration');
const { syncServerList } = require('../utils/serverListSync');
// Client patcher for custom auth server (sanasol.ws)
let clientPatcher = null;
@@ -103,12 +105,20 @@ function generateLocalTokens(uuid, name) {
}
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
// Synchronize server list on every game launch
try {
console.log('[Launcher] Synchronizing server list...');
await syncServerList();
} catch (syncError) {
console.warn('[Launcher] Server list sync failed, continuing launch:', syncError.message);
}
const branch = branchOverride || loadVersionBranch();
const customAppDir = getResolvedAppDir(installPathOverride);
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
// NEW 2.1.2: Use centralized UserData location
// NEW 2.2.0: Use centralized UserData location
const userDataDir = getUserDataPath();
const gameLatest = customGameDir;
@@ -169,7 +179,7 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
if (progressCallback && msg) {
progressCallback(msg, percent, null, null, null);
}
});
}, null, branch);
if (patchResult.success) {
console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`);
@@ -285,6 +295,55 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
const gpuEnv = setupGpuEnvironment(gpuPreference);
Object.assign(env, gpuEnv);
// Linux: Replace bundled libzstd.so with system version to fix glibc 2.41+ crash
// The bundled libzstd causes "free(): invalid pointer" on Steam Deck / Ubuntu LTS
if (process.platform === 'linux' && process.env.HYTALE_NO_LIBZSTD_FIX !== '1') {
const clientDir = path.dirname(clientPath);
const bundledLibzstd = path.join(clientDir, 'libzstd.so');
const backupLibzstd = path.join(clientDir, 'libzstd.so.bundled');
// Common system libzstd paths
const systemLibzstdPaths = [
'/usr/lib/libzstd.so.1', // Arch Linux, Steam Deck
'/usr/lib/x86_64-linux-gnu/libzstd.so.1', // Debian/Ubuntu
'/usr/lib64/libzstd.so.1' // Fedora/RHEL
];
let systemLibzstd = null;
for (const p of systemLibzstdPaths) {
if (fs.existsSync(p)) {
systemLibzstd = p;
break;
}
}
if (systemLibzstd && fs.existsSync(bundledLibzstd)) {
try {
const stats = fs.lstatSync(bundledLibzstd);
// Only replace if it's not already a symlink to system version
if (!stats.isSymbolicLink()) {
// Backup bundled version
if (!fs.existsSync(backupLibzstd)) {
fs.renameSync(bundledLibzstd, backupLibzstd);
console.log(`Linux: Backed up bundled libzstd.so`);
} else {
fs.unlinkSync(bundledLibzstd);
}
// Create symlink to system version
fs.symlinkSync(systemLibzstd, bundledLibzstd);
console.log(`Linux: Linked libzstd.so to system version (${systemLibzstd}) for glibc 2.41+ compatibility`);
} else {
const linkTarget = fs.readlinkSync(bundledLibzstd);
console.log(`Linux: libzstd.so already linked to ${linkTarget}`);
}
} catch (libzstdError) {
console.warn(`Linux: Could not replace libzstd.so: ${libzstdError.message}`);
}
}
}
try {
let spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'],
@@ -388,7 +447,13 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
const customCacheDir = path.join(customAppDir, 'cache');
try {
await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir, branch);
let versionToInstall = latestVersion;
if (FORCE_CLEAN_INSTALL_VERSION && !installedVersion) {
versionToInstall = CLEAN_INSTALL_TEST_VERSION;
console.log(`TESTING MODE: Clean install detected, forcing version ${versionToInstall} instead of ${latestVersion}`);
}
await ensureGameInstalled(versionToInstall, branch, progressCallback, customGameDir, customCacheDir, customToolsDir);
console.log('Game updated successfully, patching will be forced on launch...');
if (progressCallback) {

View File

@@ -5,13 +5,14 @@ const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursi
const { getOS, getArch } = require('../utils/platformUtils');
const { downloadFile, retryDownload, retryStalledDownload, MAX_AUTOMATIC_STALL_RETRIES } = require('../utils/fileManager');
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
const { installButler } = require('./butlerManager');
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config');
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
const { getUserDataPath, migrateUserDataToCentralized } = require('../utils/userDataMigration');
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
const osName = getOS();
const arch = getArch();
@@ -300,6 +301,16 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
fs.rmSync(stagingDir, { recursive: true, force: true });
}
// Delete PWR file from cache after successful installation
try {
if (fs.existsSync(pwrFile)) {
fs.unlinkSync(pwrFile);
console.log('[Butler] PWR file deleted from cache after successful installation:', pwrFile);
}
} catch (delErr) {
console.warn('[Butler] Failed to delete PWR file from cache:', delErr.message);
}
if (progressCallback) {
progressCallback('Installation complete', null, null, null, null);
}
@@ -316,7 +327,7 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`);
try {
// NEW 2.1.2: Ensure UserData migration to centralized location
// NEW 2.2.0: Ensure UserData migration to centralized location
try {
console.log('[UpdateGameFiles] Ensuring UserData migration...');
const migrationResult = await migrateUserDataToCentralized();
@@ -352,7 +363,15 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
}
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir);
// Delete PWR file from cache after successful update
try {
if (fs.existsSync(pwrFile)) {
fs.unlinkSync(pwrFile);
console.log('[UpdateGameFiles] PWR file deleted from cache after successful update:', pwrFile);
}
} catch (delErr) {
console.warn('[UpdateGameFiles] Failed to delete PWR file from cache:', delErr.message);
}
if (progressCallback) {
progressCallback('Replacing game files...', 80, null, null, null);
}
@@ -384,7 +403,7 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
console.log('Logo@2x.png update result after update:', logoResult);
// NEW 2.1.2: No longer create UserData in game installation
// NEW 2.2.0: No longer create UserData in game installation
// UserData is now in centralized location (getUserDataPath())
console.log('[UpdateGameFiles] UserData is managed in centralized location');
@@ -434,7 +453,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
// NEW 2.1.2: Ensure UserData migration to centralized location
// NEW 2.2.0: Ensure UserData migration to centralized location
try {
console.log('[InstallGame] Ensuring UserData migration...');
const migrationResult = await migrateUserDataToCentralized();
@@ -510,31 +529,33 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
console.log(`Installing game files for branch: ${branch}...`);
const latestVersion = await getLatestClientVersion(branch);
const targetVersion = FORCE_CLEAN_INSTALL_VERSION ? CLEAN_INSTALL_TEST_VERSION : latestVersion;
if (FORCE_CLEAN_INSTALL_VERSION) {
console.log(`TESTING MODE: Forcing installation of ${targetVersion} instead of ${latestVersion}`);
}
let pwrFile;
try {
pwrFile = await downloadPWR(branch, latestVersion, progressCallback, customCacheDir);
pwrFile = await downloadPWR(branch, targetVersion, progressCallback, customCacheDir);
// If downloadPWR returns false, it means the file doesn't exist or is invalid
// We should retry the download with a manual retry flag
if (!pwrFile) {
console.log('[Install] PWR file not found or invalid, attempting retry...');
pwrFile = await retryPWRDownload(branch, latestVersion, progressCallback, customCacheDir);
pwrFile = await retryPWRDownload(branch, targetVersion, progressCallback, customCacheDir);
}
// Double-check we have a valid file path
if (!pwrFile || typeof pwrFile !== 'string') {
throw new Error(`PWR file download failed: received invalid path ${pwrFile}. Please retry download.`);
}
} catch (downloadError) {
console.error('[Install] PWR download failed:', downloadError.message);
throw downloadError; // Re-throw to be handled by the main installGame error handler
throw downloadError;
}
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir, branch, customCacheDir);
// Save the installed version and branch to config
saveVersionClient(latestVersion);
saveVersionClient(targetVersion);
const { saveVersionBranch } = require('../core/config');
saveVersionBranch(branch);
@@ -544,7 +565,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
console.log('Logo@2x.png update result after installation:', logoResult);
// NEW 2.1.2: No longer create UserData in game installation
// NEW 2.2.0: No longer create UserData in game installation
// UserData is managed in centralized location (getUserDataPath())
console.log('[InstallGame] UserData is managed in centralized location');

View File

@@ -3,7 +3,7 @@ const path = require('path');
const crypto = require('crypto');
const axios = require('axios');
const { getOS } = require('../utils/platformUtils');
const { getModsPath, getProfilesDir } = require('../core/paths');
const { getModsPath, getProfilesDir, getHytaleSavesDir } = require('../core/paths');
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
const profileManager = require('./profileManager');
@@ -296,8 +296,9 @@ async function syncModsForCurrentProfile() {
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name} (${activeProfile.id})`);
// 1. Resolve Paths
// globalModsPath is the one the game uses (symlink target)
const globalModsPath = await getModsPath();
// centralModsPath is HytaleSaves\Mods (centralized location for active mods)
const hytaleSavesDir = getHytaleSavesDir();
const centralModsPath = path.join(hytaleSavesDir, 'Mods');
// profileModsPath is the real storage for this profile
const profileModsPath = getProfileModsPath(activeProfile.id);
const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
@@ -306,96 +307,51 @@ async function syncModsForCurrentProfile() {
fs.mkdirSync(profileDisabledModsPath, { recursive: true });
}
// 2. Symlink / Migration Logic
let needsLink = false;
let globalStats = null;
// 2. Copy-based Mod Sync (No symlinks - avoids permission issues)
// Ensure HytaleSaves\Mods directory exists
if (!fs.existsSync(centralModsPath)) {
fs.mkdirSync(centralModsPath, { recursive: true });
console.log(`[ModManager] Created centralized mods directory: ${centralModsPath}`);
}
// Check for old symlink and convert to real directory if needed (one-time migration)
try {
globalStats = fs.lstatSync(globalModsPath);
const centralStats = fs.lstatSync(centralModsPath);
if (centralStats.isSymbolicLink()) {
console.log('[ModManager] Removing old symlink, converting to copy-based system...');
fs.unlinkSync(centralModsPath);
fs.mkdirSync(centralModsPath, { recursive: true });
}
} catch (e) {
// Path doesn't exist
// Path doesn't exist, will be created above
}
if (globalStats) {
if (globalStats.isSymbolicLink()) {
const linkTarget = fs.readlinkSync(globalModsPath);
// Normalize paths for comparison
if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) {
console.log(`[ModManager] Updating symlink from ${linkTarget} to ${profileModsPath}`);
fs.unlinkSync(globalModsPath);
needsLink = true;
}
} else if (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);
for (const file of files) {
const src = path.join(globalModsPath, file);
const dest = path.join(profileModsPath, file);
// Only move if dest doesn't exist to avoid overwriting
if (!fs.existsSync(dest)) {
fs.renameSync(src, dest);
}
}
// Copy enabled mods from profile to HytaleSaves\Mods (for game to use)
console.log(`[ModManager] Copying enabled mods from ${profileModsPath} to ${centralModsPath}`);
// Also migrate DisabledMods if it exists globally
const globalDisabledPath = path.join(path.dirname(globalModsPath), 'DisabledMods');
if (fs.existsSync(globalDisabledPath) && fs.lstatSync(globalDisabledPath).isDirectory()) {
const dFiles = fs.readdirSync(globalDisabledPath);
for (const file of dFiles) {
const src = path.join(globalDisabledPath, file);
const dest = path.join(profileDisabledModsPath, file);
if (!fs.existsSync(dest)) {
fs.renameSync(src, dest);
}
}
// We can remove global DisabledMods now, as it's not used by game
try { fs.rmSync(globalDisabledPath, { recursive: true, force: true }); } catch(e) {}
}
// Remove the directory so we can link it
try {
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);
// Throw error to stop.
throw new Error('Failed to migrate mods directory. Please clear ' + globalModsPath);
}
}
} else {
needsLink = true;
}
if (needsLink) {
console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`);
// First, clear central mods folder
const existingCentralMods = fs.existsSync(centralModsPath) ? fs.readdirSync(centralModsPath) : [];
for (const file of existingCentralMods) {
const filePath = path.join(centralModsPath, file);
try {
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.');
console.error(err.message);
// Fallback: create a real directory so the game still works
if (!fs.existsSync(globalModsPath)) {
fs.mkdirSync(globalModsPath, { recursive: true });
fs.unlinkSync(filePath);
} catch (e) {
console.warn(`Failed to remove ${file} from central mods:`, e.message);
}
}
// Copy enabled mods to HytaleSaves\Mods
const enabledModFiles = fs.existsSync(profileModsPath) ? fs.readdirSync(profileModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
for (const file of enabledModFiles) {
const src = path.join(profileModsPath, file);
const dest = path.join(centralModsPath, file);
try {
fs.copyFileSync(src, dest);
console.log(`[ModManager] Copied ${file} to HytaleSaves\\Mods`);
} catch (e) {
console.error(`Failed to copy ${file}:`, e.message);
}
}
}
// 3. Auto-Repair (Download missing mods)
const profileModsSnapshot = activeProfile.mods || [];
@@ -460,7 +416,7 @@ async function syncModsForCurrentProfile() {
}
// 5. Enforce Enabled/Disabled State (Move files between Profile/Mods and Profile/DisabledMods)
// Note: Since Global/Mods IS Profile/Mods (via symlink), moving out of Profile/Mods disables it for the game.
// Note: Enabled mods are copied to HytaleSaves\Mods, disabled mods stay in Profile/DisabledMods
const disabledFiles = fs.existsSync(profileDisabledModsPath) ? fs.readdirSync(profileDisabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
const allFiles = new Set([...enabledFiles, ...disabledFiles]);

View File

@@ -1,11 +1,17 @@
const axios = require('axios');
const crypto = require('crypto');
const fs = require('fs');
const { getOS, getArch } = require('../utils/platformUtils');
const BASE_PATCH_URL = 'https://game-patches.hytale.com/patches';
const MANIFEST_API = 'https://files.hytalef2p.com/api/patch_manifest';
async function getLatestClientVersion(branch = 'release') {
try {
console.log(`Fetching latest client version from API (branch: ${branch})...`);
const response = await axios.get('https://files.hytalef2p.com/api/version_client', {
params: { branch },
timeout: 5000,
timeout: 40000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
@@ -16,16 +22,144 @@ async function getLatestClientVersion(branch = 'release') {
console.log(`Latest client version for ${branch}: ${version}`);
return version;
} else {
console.log('Warning: Invalid API response, falling back to default version');
return '4.pwr';
console.log('Warning: Invalid API response, falling back to latest known version (7.pwr)');
return '7.pwr';
}
} catch (error) {
console.error('Error fetching client version:', error.message);
console.log('Warning: API unavailable, falling back to default version');
return '4.pwr';
console.log('Warning: API unavailable, falling back to latest known version (7.pwr)');
return '7.pwr';
}
}
function buildArchiveUrl(buildNumber, branch = 'release') {
const os = getOS();
const arch = getArch();
return `${BASE_PATCH_URL}/${os}/${arch}/${branch}/0/${buildNumber}.pwr`;
}
async function checkArchiveExists(buildNumber, branch = 'release') {
const url = buildArchiveUrl(buildNumber, branch);
try {
const response = await axios.head(url, { timeout: 10000 });
return response.status === 200;
} catch (error) {
return false;
}
}
async function discoverAvailableVersions(latestKnown, branch = 'release', maxProbe = 50) {
const available = [];
const latest = parseInt(latestKnown.replace('.pwr', ''));
for (let i = latest; i >= Math.max(1, latest - maxProbe); i--) {
const exists = await checkArchiveExists(i, branch);
if (exists) {
available.push(`${i}.pwr`);
}
}
return available;
}
async function fetchPatchManifest(branch = 'release') {
try {
const os = getOS();
const arch = getArch();
const response = await axios.get(MANIFEST_API, {
params: { branch, os, arch },
timeout: 10000
});
return response.data.patches || {};
} catch (error) {
console.error('Failed to fetch patch manifest:', error.message);
return {};
}
}
async function extractVersionDetails(targetVersion, branch = 'release') {
const buildNumber = parseInt(targetVersion.replace('.pwr', ''));
const previousBuild = buildNumber - 1;
const manifest = await fetchPatchManifest(branch);
const patchInfo = manifest[buildNumber];
return {
version: targetVersion,
buildNumber: buildNumber,
buildName: `HYTALE-Build-${buildNumber}`,
fullUrl: patchInfo?.original_url || buildArchiveUrl(buildNumber, branch),
differentialUrl: patchInfo?.patch_url || null,
checksum: patchInfo?.patch_hash || null,
sourceVersion: patchInfo?.from ? `${patchInfo.from}.pwr` : (previousBuild > 0 ? `${previousBuild}.pwr` : null),
isDifferential: !!patchInfo?.proper_patch,
releaseNotes: patchInfo?.patch_note || null
};
}
function canUseDifferentialUpdate(currentVersion, targetDetails) {
if (!targetDetails) return false;
if (!targetDetails.differentialUrl) return false;
if (!targetDetails.isDifferential) return false;
if (!currentVersion) return false;
const currentBuild = parseInt(currentVersion.replace('.pwr', ''));
const expectedSource = parseInt(targetDetails.sourceVersion?.replace('.pwr', '') || '0');
return currentBuild === expectedSource;
}
function needsIntermediatePatches(currentVersion, targetVersion) {
if (!currentVersion) return [];
const current = parseInt(currentVersion.replace('.pwr', ''));
const target = parseInt(targetVersion.replace('.pwr', ''));
const intermediates = [];
for (let i = current + 1; i <= target; i++) {
intermediates.push(`${i}.pwr`);
}
return intermediates;
}
async function computeFileChecksum(filePath) {
return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath);
stream.on('data', data => hash.update(data));
stream.on('end', () => resolve(hash.digest('hex')));
stream.on('error', reject);
});
}
async function validateChecksum(filePath, expectedChecksum) {
if (!expectedChecksum) return true;
const actualChecksum = await computeFileChecksum(filePath);
return actualChecksum === expectedChecksum;
}
function getInstalledClientVersion() {
try {
const { loadVersionClient } = require('../core/config');
return loadVersionClient();
} catch (err) {
return null;
}
}
module.exports = {
getLatestClientVersion
getLatestClientVersion,
buildArchiveUrl,
checkArchiveExists,
discoverAvailableVersions,
extractVersionDetails,
canUseDifferentialUpdate,
needsIntermediatePatches,
computeFileChecksum,
validateChecksum,
getInstalledClientVersion
};

File diff suppressed because it is too large Load Diff

View File

@@ -18,11 +18,16 @@ function isWaylandSession() {
}
const sessionType = process.env.XDG_SESSION_TYPE;
const waylandDisplay = process.env.WAYLAND_DISPLAY;
// Debug logging
console.log(`[PlatformUtils] Checking Wayland: XDG_SESSION_TYPE=${sessionType}, WAYLAND_DISPLAY=${waylandDisplay}`);
if (sessionType && sessionType.toLowerCase() === 'wayland') {
return true;
}
if (process.env.WAYLAND_DISPLAY) {
if (waylandDisplay) {
return true;
}
@@ -45,18 +50,47 @@ function setupWaylandEnvironment() {
return {};
}
// If the user has manually set SDL_VIDEODRIVER (e.g. to 'x11'), strictly respect it.
if (process.env.SDL_VIDEODRIVER) {
console.log(`User manually set SDL_VIDEODRIVER=${process.env.SDL_VIDEODRIVER}, ignoring internal Wayland configuration.`);
return {};
}
if (!isWaylandSession()) {
console.log('Detected X11 session, using default environment');
return {};
}
console.log('Detected Wayland session, configuring environment...');
console.log('Detected Wayland session, checking for Gamescope/Steam Deck...');
const envVars = {
SDL_VIDEODRIVER: 'wayland'
};
const envVars = {};
envVars.ELECTRON_OZONE_PLATFORM_HINT = 'wayland';
// Only set Ozone hint if not already set by user
if (!process.env.ELECTRON_OZONE_PLATFORM_HINT) {
envVars.ELECTRON_OZONE_PLATFORM_HINT = 'wayland';
}
// 2. DETECT GAMESCOPE / STEAM DECK
// Native Wayland often fails for SDL games in Gaming Mode (gamescope), so we force X11 (XWayland).
// Checks:
// - XDG_CURRENT_DESKTOP == 'gamescope'
// - SteamDeck=1 (often set in SteamOS)
const currentDesktop = process.env.XDG_CURRENT_DESKTOP || '';
const isGamescope = currentDesktop.toLowerCase() === 'gamescope' || process.env.SteamDeck === '1';
if (isGamescope) {
console.log('Gamescope / Steam Deck detected, forcing SDL_VIDEODRIVER=x11 for compatibility');
envVars.SDL_VIDEODRIVER = 'x11';
} else {
// For standard desktop Wayland (GNOME, KDE), we leave SDL_VIDEODRIVER unset.
// This allows SDL3/SDL2 to use its internal preference (Wayland > X11).
// EXCEPT if it was somehow force-set to 'wayland' by the parent process (rare but possible),
// we strictly want to allow fallback, so we might unset it if it was 'wayland'.
// But since we checked process.env.SDL_VIDEODRIVER at the start, we know it's NOT set manually.
// So we effectively do nothing for standard Wayland, letting SDL decide.
console.log('Standard Wayland session detected, letting SDL decide backend (auto-fallback enabled).');
}
console.log('Wayland environment variables:', envVars);
return envVars;

View File

@@ -0,0 +1,120 @@
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
const { getHytaleSavesDir } = require('../core/paths');
const SERVER_LIST_URL = 'https://assets.authbp.xyz/server.json';
function getLocalDateTime() {
return formatLocalDateTime(new Date());
}
function formatLocalDateTime(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
const offsetMinutes = -date.getTimezoneOffset();
const offsetHours = Math.floor(Math.abs(offsetMinutes) / 60);
const offsetMins = Math.abs(offsetMinutes) % 60;
const offsetSign = offsetMinutes >= 0 ? '+' : '-';
const offset = `${offsetSign}${String(offsetHours).padStart(2, '0')}:${String(offsetMins).padStart(2, '0')}`;
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}0000${offset}`;
}
async function syncServerList() {
try {
const hytaleSavesDir = getHytaleSavesDir();
const serverListPath = path.join(hytaleSavesDir, 'ServerList.json');
console.log('[ServerListSync] Fetching server list from', SERVER_LIST_URL);
let remoteData;
try {
const response = await axios.get(SERVER_LIST_URL, {
timeout: 40000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
remoteData = response.data;
} catch (fetchError) {
console.warn('[ServerListSync] Failed to fetch remote server list:', fetchError.message);
remoteData = { SavedServers: [] };
}
let localData = { SavedServers: [] };
if (fs.existsSync(serverListPath)) {
try {
const localContent = fs.readFileSync(serverListPath, 'utf-8');
localData = JSON.parse(localContent);
console.log('[ServerListSync] Loaded existing local server list with', localData.SavedServers?.length || 0, 'servers');
} catch (parseError) {
console.warn('[ServerListSync] Failed to parse local server list, creating new one:', parseError.message);
localData = { SavedServers: [] };
}
} else {
console.log('[ServerListSync] Local server list does not exist, creating new one');
}
if (!localData.SavedServers) {
localData.SavedServers = [];
}
if (!remoteData.SavedServers) {
remoteData.SavedServers = [];
}
const existingServersByAddress = new Map();
const userServers = [];
for (const server of localData.SavedServers) {
existingServersByAddress.set(server.Address.toLowerCase(), server);
}
const remoteAddresses = new Set(remoteData.SavedServers.map(s => s.Address.toLowerCase()));
for (const server of localData.SavedServers) {
if (!remoteAddresses.has(server.Address.toLowerCase())) {
userServers.push(server);
}
}
const currentDate = getLocalDateTime();
const apiServers = [];
for (const remoteServer of remoteData.SavedServers) {
const serverToAdd = {
Id: uuidv4(),
Name: "@ " + remoteServer.Name,
Address: remoteServer.Address,
DateSaved: currentDate,
img_Banner: remoteServer.img_Banner || null // Copy banner if exists
};
apiServers.push(serverToAdd);
console.log('[ServerListSync] Added/Updated server with new ID:', remoteServer.Name);
}
localData.SavedServers = [...apiServers, ...userServers];
const addedCount = apiServers.length;
if (!fs.existsSync(hytaleSavesDir)) {
fs.mkdirSync(hytaleSavesDir, { recursive: true });
}
fs.writeFileSync(serverListPath, JSON.stringify(localData, null, 2), 'utf-8');
console.log('[ServerListSync] Server list synchronized:', addedCount, 'API servers added, total:', localData.SavedServers.length);
return { success: true, added: addedCount, total: localData.SavedServers.length };
} catch (error) {
console.error('[ServerListSync] Failed to synchronize server list:', error.message);
return { success: false, error: error.message };
}
}
module.exports = {
syncServerList
};

View File

@@ -4,7 +4,7 @@ const { getHytaleSavesDir, getResolvedAppDir } = require('../core/paths');
const { loadConfig, saveConfig } = require('../core/config');
/**
* NEW SYSTEM (2.1.2+): UserData Migration to Centralized Location
* NEW SYSTEM (2.2.0+): UserData Migration to Centralized Location
*
* UserData is now stored in a centralized location instead of inside game installation:
* - Windows: %LOCALAPPDATA%\HytaleSaves\
@@ -31,7 +31,7 @@ function markMigrationCompleted() {
}
/**
* Find old UserData location (pre-2.1.2)
* Find old UserData location (pre-2.2.0)
* Searches in: installPath/branch/package/game/latest/Client/UserData
*/
function findOldUserDataPath() {
@@ -77,7 +77,7 @@ function findOldUserDataPath() {
/**
* Migrate UserData from old location to new centralized location
* One-time operation when upgrading to 2.1.2
* One-time operation when upgrading to 2.2.0
*/
async function migrateUserDataToCentralized() {
// Check if already migrated
@@ -149,7 +149,7 @@ async function migrateUserDataToCentralized() {
}
/**
* Get the centralized UserData path (always use this in 2.1.2+)
* Get the centralized UserData path (always use this in 2.2.0+)
* Ensures directory exists
*/
function getUserDataPath() {

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

View File

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

View File

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

BIN
icon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

106
main.js
View File

@@ -3,7 +3,7 @@ require('dotenv').config({ path: path.join(__dirname, '.env') });
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
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 { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched, loadConfig, saveConfig } = require('./backend/launcher');
const { retryPWRDownload } = require('./backend/managers/gameManager');
const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration');
@@ -86,6 +86,10 @@ function setDiscordActivity() {
{
label: 'GitHub',
url: 'https://github.com/amiayweb/Hytale-F2P'
},
{
label: 'Discord',
url: 'https://discord.gg/hf2pdc'
}
]
});
@@ -176,7 +180,8 @@ function createWindow() {
initDiscordRPC();
// Configure and initialize electron-updater
autoUpdater.autoDownload = false;
// Enable auto-download so updates start immediately when available
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.on('checking-for-update', () => {
@@ -201,6 +206,20 @@ function createWindow() {
autoUpdater.on('error', (err) => {
console.error('Error in auto-updater:', err);
// Handle macOS code signing errors - requires manual download
if (mainWindow && !mainWindow.isDestroyed()) {
const isMacSigningError = process.platform === 'darwin' &&
(err.code === 'ERR_UPDATER_INVALID_SIGNATURE' ||
err.message.includes('signature') ||
err.message.includes('code sign'));
mainWindow.webContents.send('update-error', {
message: err.message,
isMacSigningError: isMacSigningError,
requiresManualDownload: isMacSigningError || process.platform === 'darwin'
});
}
});
autoUpdater.on('download-progress', (progressObj) => {
@@ -218,7 +237,10 @@ function createWindow() {
console.log('Update downloaded:', info.version);
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-downloaded', {
version: info.version
version: info.version,
platform: process.platform,
// macOS auto-install often fails on unsigned apps
autoInstallSupported: process.platform !== 'darwin'
});
}
});
@@ -529,7 +551,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
console.log('[Main] Processing Butler error with retry context');
errorData.retryData = {
branch: error.branch || 'release',
fileName: error.fileName || '4.pwr',
fileName: error.fileName || '7.pwr',
cacheDir: error.cacheDir
};
errorData.canRetry = error.canRetry !== false;
@@ -549,7 +571,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
console.log('[Main] Processing generic error, creating default retry data');
errorData.retryData = {
branch: 'release',
fileName: '4.pwr'
fileName: '7.pwr'
};
// For generic errors, assume it's retryable unless specified
errorData.canRetry = error.canRetry !== false;
@@ -581,22 +603,6 @@ ipcMain.handle('save-username', (event, username) => {
ipcMain.handle('load-username', () => {
return loadUsername();
});
ipcMain.handle('save-chat-username', async (event, chatUsername) => {
saveChatUsername(chatUsername);
});
ipcMain.handle('load-chat-username', async () => {
return loadChatUsername();
});
ipcMain.handle('save-chat-color', (event, color) => {
saveChatColor(color);
return { success: true };
});
ipcMain.handle('load-chat-color', () => {
return loadChatColor();
});
ipcMain.handle('save-java-path', (event, javaPath) => {
saveJavaPath(javaPath);
@@ -654,6 +660,15 @@ ipcMain.handle('load-launcher-hw-accel', () => {
return loadLauncherHardwareAcceleration();
});
ipcMain.handle('load-config', () => {
return loadConfig();
});
ipcMain.handle('save-config', (event, configUpdate) => {
saveConfig(configUpdate);
return { success: true };
});
ipcMain.handle('select-install-path', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
@@ -784,7 +799,7 @@ ipcMain.handle('retry-download', async (event, retryData) => {
console.log('[IPC] Invalid retry data, using PWR defaults');
retryData = {
branch: 'release',
fileName: '4.pwr'
fileName: '7.pwr'
};
}
@@ -818,7 +833,7 @@ ipcMain.handle('retry-download', async (event, retryData) => {
} :
{
branch: retryData?.branch || 'release',
fileName: retryData?.fileName || '4.pwr',
fileName: retryData?.fileName || '7.pwr',
cacheDir: retryData?.cacheDir
};
@@ -859,6 +874,17 @@ ipcMain.handle('open-external', async (event, url) => {
}
});
ipcMain.handle('open-download-page', async () => {
try {
// Open GitHub releases page for manual download
await shell.openExternal('https://github.com/amiayweb/Hytale-F2P/releases/latest');
return { success: true };
} catch (error) {
console.error('Failed to open download page:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('open-game-location', async () => {
try {
const { getResolvedAppDir, loadVersionBranch } = require('./backend/launcher');
@@ -1086,8 +1112,37 @@ ipcMain.handle('download-update', async () => {
}
});
ipcMain.handle('install-update', () => {
autoUpdater.quitAndInstall(false, true);
ipcMain.handle('install-update', async () => {
console.log('[AutoUpdater] Installing update...');
// On macOS, quitAndInstall often fails silently
// Use a more aggressive approach
if (process.platform === 'darwin') {
console.log('[AutoUpdater] macOS detected, using force quit approach');
// Give user feedback that something is happening
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-installing');
}
// Small delay to show the "Installing..." state
await new Promise(resolve => setTimeout(resolve, 500));
try {
autoUpdater.quitAndInstall(false, true);
} catch (err) {
console.error('[AutoUpdater] quitAndInstall failed:', err);
// Force quit the app - the update should install on next launch
app.exit(0);
}
// If quitAndInstall didn't work, force exit after a delay
setTimeout(() => {
console.log('[AutoUpdater] Force exiting app...');
app.exit(0);
}, 2000);
} else {
autoUpdater.quitAndInstall(false, true);
}
});
ipcMain.handle('get-launcher-version', () => {
@@ -1321,3 +1376,4 @@ ipcMain.handle('profile-update', async (event, id, updates) => {
return { error: error.message };
}
});

5
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "hytale-f2p-launcher",
"version": "2.1.1",
"version": "2.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hytale-f2p-launcher",
"version": "2.1.1",
"version": "2.1.2",
"license": "MIT",
"dependencies": {
"adm-zip": "^0.5.10",
@@ -19,6 +19,7 @@
"uuid": "^9.0.1"
},
"devDependencies": {
"@electron/notarize": "^2.5.0",
"electron": "^40.0.0",
"electron-builder": "^26.4.0"
}

View File

@@ -45,6 +45,7 @@
},
"license": "MIT",
"devDependencies": {
"@electron/notarize": "^2.5.0",
"electron": "^40.0.0",
"electron-builder": "^26.4.0"
},
@@ -131,7 +132,15 @@
}
],
"icon": "build/icon.icns",
"category": "public.app-category.games"
"category": "public.app-category.games",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist",
"forceCodeSigning": true,
"strictVerify": true,
"type": "distribution",
"notarize": true
},
"nsis": {
"oneClick": false,

View File

@@ -9,10 +9,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
getVersion: () => ipcRenderer.invoke('get-version'),
saveUsername: (username) => ipcRenderer.invoke('save-username', username),
loadUsername: () => ipcRenderer.invoke('load-username'),
saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername),
loadChatUsername: () => ipcRenderer.invoke('load-chat-username'),
saveChatColor: (chatColor) => ipcRenderer.invoke('save-chat-color', chatColor),
loadChatColor: () => ipcRenderer.invoke('load-chat-color'),
saveJavaPath: (javaPath) => ipcRenderer.invoke('save-java-path', javaPath),
loadJavaPath: () => ipcRenderer.invoke('load-java-path'),
saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath),
@@ -23,8 +19,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
loadLanguage: () => ipcRenderer.invoke('load-language'),
saveCloseLauncher: (enabled) => ipcRenderer.invoke('save-close-launcher', enabled),
loadCloseLauncher: () => ipcRenderer.invoke('load-close-launcher'),
loadConfig: () => ipcRenderer.invoke('load-config'),
saveConfig: (configUpdate) => ipcRenderer.invoke('save-config', configUpdate),
// Harwadre Acceleration
// Hardware Acceleration
saveLauncherHardwareAcceleration: (enabled) => ipcRenderer.invoke('save-launcher-hw-accel', enabled),
loadLauncherHardwareAcceleration: () => ipcRenderer.invoke('load-launcher-hw-accel'),
@@ -50,14 +48,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
selectModFiles: () => ipcRenderer.invoke('select-mod-files'),
copyModFile: (sourcePath, modsPath) => ipcRenderer.invoke('copy-mod-file', sourcePath, modsPath),
onProgressUpdate: (callback) => {
ipcRenderer.on('progress-update', (event, data) => {
// Ensure data includes retry state if available
if (data && typeof data === 'object') {
callback(data);
} else {
callback(data);
}
});
ipcRenderer.on('progress-update', (event, data) => callback(data));
},
onProgressComplete: (callback) => {
ipcRenderer.on('progress-complete', () => callback());
@@ -69,7 +60,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.on('installation-end', () => callback());
},
getUserId: () => ipcRenderer.invoke('get-user-id'),
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
openDownloadPage: () => ipcRenderer.invoke('open-download-page'),
getUpdateInfo: () => ipcRenderer.invoke('get-update-info'),
onUpdatePopup: (callback) => {
@@ -126,6 +116,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
downloadUpdate: () => ipcRenderer.invoke('download-update'),
installUpdate: () => ipcRenderer.invoke('install-update'),
quitAndInstallUpdate: () => ipcRenderer.invoke('install-update'), // Alias for update.js compatibility
getLauncherVersion: () => ipcRenderer.invoke('get-launcher-version'),
onUpdateAvailable: (callback) => {
ipcRenderer.on('update-available', (event, data) => callback(data));

92
scripts/notarize.js Normal file
View File

@@ -0,0 +1,92 @@
console.log('[Notarize] Script loaded');
let notarize;
try {
notarize = require('@electron/notarize').notarize;
console.log('[Notarize] @electron/notarize loaded successfully');
} catch (err) {
console.error('[Notarize] Failed to load @electron/notarize:', err.message);
throw err;
}
const path = require('path');
// Timeout for notarization (30 minutes max)
const NOTARIZE_TIMEOUT_MS = 30 * 60 * 1000;
function withTimeout(promise, ms, message) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(message)), ms)
)
]);
}
exports.default = async function notarizing(context) {
console.log('[Notarize] afterSign hook called');
console.log('[Notarize] Context:', JSON.stringify({
platform: context.electronPlatformName,
appOutDir: context.appOutDir,
outDir: context.outDir
}, null, 2));
const { electronPlatformName, appOutDir } = context;
// Only notarize macOS builds
if (electronPlatformName !== 'darwin') {
console.log('[Notarize] Skipping: not macOS');
return;
}
// Check if notarization is disabled via env var
if (process.env.SKIP_NOTARIZE === 'true') {
console.log('[Notarize] Skipping: SKIP_NOTARIZE=true');
return;
}
// Check credentials
const hasAppleId = !!process.env.APPLE_ID;
const hasPassword = !!process.env.APPLE_APP_SPECIFIC_PASSWORD;
const hasTeamId = !!process.env.APPLE_TEAM_ID;
console.log('[Notarize] Credentials check:', { hasAppleId, hasPassword, hasTeamId });
if (!hasAppleId || !hasPassword || !hasTeamId) {
console.log('[Notarize] Skipping: missing credentials');
return;
}
const appName = context.packager.appInfo.productFilename;
const appPath = path.join(appOutDir, `${appName}.app`);
console.log('[Notarize] Starting notarization...');
console.log('[Notarize] App path:', appPath);
console.log('[Notarize] Team ID:', process.env.APPLE_TEAM_ID);
console.log('[Notarize] Timeout:', NOTARIZE_TIMEOUT_MS / 1000, 'seconds');
try {
await withTimeout(
notarize({
appPath,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
teamId: process.env.APPLE_TEAM_ID,
}),
NOTARIZE_TIMEOUT_MS,
`Notarization timed out after ${NOTARIZE_TIMEOUT_MS / 1000} seconds`
);
console.log('[Notarize] Notarization complete!');
} catch (error) {
console.error('[Notarize] Notarization failed:', error.message);
// Don't fail the build if notarization times out or fails
// The app will still be code-signed, just not notarized
if (process.env.NOTARIZE_FAIL_ON_ERROR !== 'true') {
console.warn('[Notarize] Continuing build without notarization (set NOTARIZE_FAIL_ON_ERROR=true to fail)');
return;
}
throw error;
}
};