199 Commits

Author SHA1 Message Date
AMIAY
46ed4cb2be Add project status and community links to README 2026-02-19 09:21:09 +01:00
AMIAY
6369fd7669 Remove all files 2026-02-19 04:05:00 +01:00
AMIAY
244fec0c77 Update Discord link for downloading latest version 2026-02-11 22:27:09 +01:00
AMIAY
ebc11de314 Revise problem reporting instructions in README (#284)
Updated warning message regarding Discord support and GitHub repo issues.
2026-02-11 22:25:58 +01:00
AMIAY
90448e200e Revise problem reporting instructions in README
Updated warning message regarding Discord support and GitHub repo issues.
2026-02-11 22:25:36 +01:00
AMIAY
e5b44341f1 Develop (#282)
* 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

* fix: preserves arch x64 on linux target for #242

* fix: removed arm64 flags

* fix: redo package.json arch

* update package-lock.json

* Update release.yml

* chore: sync package-lock with package.json

* fix: reorder fedora libzstd paths to first iteration

* feat: enhance gpu detection, drafting

* fix: comprehensive UUID/username persistence bug fixes (#252)

* fix: comprehensive UUID/username persistence bug fixes

Major fixes for UUID/skin reset issues that caused players to lose cosmetics:

Core fixes:
- Username rename now preserves UUID (atomic rename, not new identity)
- Atomic config writes with backup/recovery system
- Case-insensitive UUID lookup with case-preserving storage
- Pre-launch validation blocks play if no username configured
- Removed saveUsername calls from launch/install flows

UUID Modal fixes:
- Fixed isCurrent badge showing on wrong user
- Added switch identity button to change between saved usernames
- Fixed custom UUID input using unsaved DOM username
- UUID list now refreshes when player name changes
- Enabled copy/paste in custom UUID input field

UI/UX improvements:
- Added translation keys for switch username functionality
- CSS user-select fix for UUID input fields
- Allowed Ctrl+V/C/X/A shortcuts in Electron

Files: config.js, gameLauncher.js, gameManager.js, playerManager.js,
launcher.js, settings.js, main.js, preload.js, style.css, en.json

See UUID_BUGS_FIX_PLAN.md for detailed bug list (18 bugs, 16 fixed)

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

* feat(i18n): add switch username translations to all locales

Added translation keys for username switching functionality:
- notifications.noUsername
- notifications.switchUsernameSuccess
- notifications.switchUsernameFailed
- notifications.playerNameTooLong
- confirm.switchUsernameTitle
- confirm.switchUsernameMessage
- confirm.switchUsernameButton

Languages updated: de-DE, es-ES, fr-FR, id-ID, pl-PL, pt-BR, ru-RU, sv-SE, tr-TR

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

* docs: move UUID_BUGS_FIX_PLAN.md to docs folder

* docs: update UUID_BUGS_FIX_PLAN with complete fix details

---------

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

* chore: rearrange, fix, and improve README.md

* chore: link downloads, platform, and version to release page in README.md

* chore: update discord link

* chore: insert contact link in CODE_OF_CONDUCT.md

* fix: missing version text on launcher

* chore: update quickstart button link to header

* chore: update discord link and give warning quickstart

* chore revise online play hosting instructions in README

Updated instructions for hosting an online game and clarified troubleshooting steps.

* Fix Turkish translations in tr-TR.json

* fix: EPERM error in Repair Game Button [windows testing needed]

* fix: invalid generated token that caused hangs on exit [windows testing needed]

* fix: major bug - hytale won't launch with laptop machine and ghost processes

* fix: discord RPC destroy error if not connected

* fix: major bug - detach game process to avoid launcher-held handles causing zombie process

* docs: add analysis on ghost process and launcher cleanup

* revert generateLocalTokens, wrong analysis on game launching issue

* revert add deps for generateLocalTokens

* Add proxy client and route downloads through it

* fix: Prevent JAR file corruption during proxy downloads

Fixed binary file corruption when downloading through proxy by using PassThrough stream to preserve data integrity while tracking download progress.

* Improve featured servers layout with Discord integration

- Add Discord button to server cards when discord link is present in API data
- Remove HF2P Servers section to use full width for featured servers
- Increase server card size (300x180px banner, larger fonts and spacing)
- Simplify layout from 2-column grid to single full-width container
- Discord button opens external browser with server invite link

* package version to 2.2.1

Update package.json version from 2.2.0 to 2.2.1 to publish a patch release.

* fix: add game_running_marker to prevent duplicate launches

* Add smart proxy with direct-fallback and logging

* fix: remove duplicate check

* fix: cache invalidation from .env prevents multiple launch attempts

for all env related, it is necessary to clear cache first, otherwise on few launch attempts the game wouldn't run

* fix: redact proxy_url and remove timed out emoji

* Prepare Release v2.2.1

* docs: enhance bug report template with placeholders and options

Updated the bug report template to include placeholders and additional Linux distributions.

* chore revise windows prerequisites and changelog

Updated prerequisites and changelog for version 2.2.1.

* chore: improvise badges, relocate star history, fix discord links

* chore: fix release notes for v2.2.1

* feat(macos): add code signing and notarization support

Add macOS code signing and notarization for Gatekeeper compatibility:

- Add hardened runtime configuration in package.json
- Add entitlements.mac.plist for required app permissions
- Enable built-in electron-builder notarization
- Add code signing and notarization secrets to workflow

Required GitHub Secrets:
- CSC_LINK: Base64-encoded .p12 certificate file
- CSC_KEY_PASSWORD: Password for the .p12 certificate
- APPLE_ID: Apple Developer account email
- APPLE_APP_SPECIFIC_PASSWORD: App-specific password from appleid.apple.com
- APPLE_TEAM_ID: 10-character Apple Developer Team ID

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

* Revise and enhance Hytale F2P Server Guide

Updated the Hytale F2P Server Guide with new sections and improved formatting.

* Update SERVER.md official accounts info

Added CloudNord hosting information and new section for playing online with official accounts.

* Update SERVER.md

* refactor: replace pre-patched JAR download with ByteBuddy agent

Migrate from downloading pre-patched server JARs from CDN to downloading
the DualAuth ByteBuddy Agent from GitHub releases. The server JAR stays
pristine - auth patching happens at runtime via -javaagent: flag.

clientPatcher.js:
- Replace patchServer() with ensureAgentAvailable()
- Download dualauth-agent.jar to Server/ directory
- Remove serverJarContainsDualAuth() and validateServerJarSize()

gameLauncher.js:
- Set JAVA_TOOL_OPTIONS env var with -javaagent: for runtime patching
- Update logging to show agent status instead of server patch count

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

* Bump package version to 2.2.2

Update package.json version from 2.2.1 to 2.2.2 to mark a patch release.

* Use new version API and default to v8 PWR

---------

Co-authored-by: Fazri Gading <fazrigading@gmail.com>
Co-authored-by: TalesAmaral <57869141+TalesAmaral@users.noreply.github.com>
Co-authored-by: walti0 <95646872+walti0@users.noreply.github.com>
Co-authored-by: Fazri Gading <super.fai700@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>
Co-authored-by: MetricsLite <66024355+MetricsLite@users.noreply.github.com>
2026-02-11 11:13:14 +01:00
AMIAY
ae6a7db80a Update Discord link in README.md 2026-02-09 11:41:22 +01:00
AMIAY
48395fbff3 Merge pull request #278 from amiayweb/amiayweb-patch-1
Update Discord link for community help
2026-02-09 11:30:28 +01:00
AMIAY
aae90a72e8 Update Discord link for community help 2026-02-09 11:30:09 +01:00
AMIAY
a6c61aef68 Merge pull request #259 from amiayweb/fix/release-2-2-1-env 2026-02-02 08:05:00 +01:00
Fazri Gading
31653a37a7 fix: release v2.2.1 add virtual .env file creation in release workflow
Added steps to create a virtual .env file for different platforms during the release process.
2026-02-02 14:44:33 +08:00
Fazri Gading
1cb08f029a Release Stable Build v2.2.1 (#258)
* 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

* fix: preserves arch x64 on linux target for #242

* fix: removed arm64 flags

* fix: redo package.json arch

* update package-lock.json

* Update release.yml

* chore: sync package-lock with package.json

* fix: reorder fedora libzstd paths to first iteration

* feat: enhance gpu detection, drafting

* fix: comprehensive UUID/username persistence bug fixes (#252)

* fix: comprehensive UUID/username persistence bug fixes

Major fixes for UUID/skin reset issues that caused players to lose cosmetics:

Core fixes:
- Username rename now preserves UUID (atomic rename, not new identity)
- Atomic config writes with backup/recovery system
- Case-insensitive UUID lookup with case-preserving storage
- Pre-launch validation blocks play if no username configured
- Removed saveUsername calls from launch/install flows

UUID Modal fixes:
- Fixed isCurrent badge showing on wrong user
- Added switch identity button to change between saved usernames
- Fixed custom UUID input using unsaved DOM username
- UUID list now refreshes when player name changes
- Enabled copy/paste in custom UUID input field

UI/UX improvements:
- Added translation keys for switch username functionality
- CSS user-select fix for UUID input fields
- Allowed Ctrl+V/C/X/A shortcuts in Electron

Files: config.js, gameLauncher.js, gameManager.js, playerManager.js,
launcher.js, settings.js, main.js, preload.js, style.css, en.json

See UUID_BUGS_FIX_PLAN.md for detailed bug list (18 bugs, 16 fixed)

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

* feat(i18n): add switch username translations to all locales

Added translation keys for username switching functionality:
- notifications.noUsername
- notifications.switchUsernameSuccess
- notifications.switchUsernameFailed
- notifications.playerNameTooLong
- confirm.switchUsernameTitle
- confirm.switchUsernameMessage
- confirm.switchUsernameButton

Languages updated: de-DE, es-ES, fr-FR, id-ID, pl-PL, pt-BR, ru-RU, sv-SE, tr-TR

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

* docs: move UUID_BUGS_FIX_PLAN.md to docs folder

* docs: update UUID_BUGS_FIX_PLAN with complete fix details

---------

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

* chore: rearrange, fix, and improve README.md

* chore: link downloads, platform, and version to release page in README.md

* chore: update discord link

* chore: insert contact link in CODE_OF_CONDUCT.md

* fix: missing version text on launcher

* chore: update quickstart button link to header

* chore: update discord link and give warning quickstart

* chore revise online play hosting instructions in README

Updated instructions for hosting an online game and clarified troubleshooting steps.

* Fix Turkish translations in tr-TR.json

* fix: EPERM error in Repair Game Button [windows testing needed]

* fix: invalid generated token that caused hangs on exit [windows testing needed]

* fix: major bug - hytale won't launch with laptop machine and ghost processes

* fix: discord RPC destroy error if not connected

* fix: major bug - detach game process to avoid launcher-held handles causing zombie process

* docs: add analysis on ghost process and launcher cleanup

* revert generateLocalTokens, wrong analysis on game launching issue

* revert add deps for generateLocalTokens

* Add proxy client and route downloads through it

* fix: Prevent JAR file corruption during proxy downloads

Fixed binary file corruption when downloading through proxy by using PassThrough stream to preserve data integrity while tracking download progress.

* Improve featured servers layout with Discord integration

- Add Discord button to server cards when discord link is present in API data
- Remove HF2P Servers section to use full width for featured servers
- Increase server card size (300x180px banner, larger fonts and spacing)
- Simplify layout from 2-column grid to single full-width container
- Discord button opens external browser with server invite link

* package version to 2.2.1

Update package.json version from 2.2.0 to 2.2.1 to publish a patch release.

* fix: add game_running_marker to prevent duplicate launches

* Add smart proxy with direct-fallback and logging

* fix: remove duplicate check

* fix: cache invalidation from .env prevents multiple launch attempts

for all env related, it is necessary to clear cache first, otherwise on few launch attempts the game wouldn't run

* fix: redact proxy_url and remove timed out emoji

* Prepare Release v2.2.1

* docs: enhance bug report template with placeholders and options

Updated the bug report template to include placeholders and additional Linux distributions.

* chore revise windows prerequisites and changelog

Updated prerequisites and changelog for version 2.2.1.

* chore: improvise badges, relocate star history, fix discord links

* chore: fix release notes for v2.2.1

---------

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>
Co-authored-by: MetricsLite <66024355+MetricsLite@users.noreply.github.com>
2026-02-02 05:58:56 +01:00
AMIAY
6a66ed831c Merge pull request #251 from amiayweb/fix/arch-linux-build-fail
fix: missing encoding package dependency in package-lock
2026-01-31 16:34:43 +01:00
Fazri Gading
78bb10588d fix: missing encoding package dependency in package-lock 2026-01-31 23:31:30 +08:00
AMIAY
d27663a1ce revert 2026-01-31 16:23:35 +01:00
AMIAY
2db7d606bd Add 'encoding' dependency for test 2026-01-31 16:22:02 +01:00
Fazri Gading
39c12c0591 Release v2.2.0 - Fixed Arch failed build for Linux (#250)
* 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

* fix: preserves arch x64 on linux target for #242

* fix: removed arm64 flags

* fix: redo package.json arch

* update package-lock.json

* Update release.yml

* chore: sync package-lock with package.json

---------

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-31 23:08:40 +08:00
Fazri Gading
094bb938fc Release v2.2.0 - Fix AppImage Build (#247) 2026-01-31 15:26:44 +01: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
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
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
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
Fazri Gading
faf21b830b Merge pull request #196 from amiayweb/develop
Release v2.1.1: fix EPERM error and add ArchLinux package (.pkg.tar.zst)
2026-01-27 02:29:35 +08:00
Fazri Gading
6f10b1390d Release v2.1.1: fix EPERM error and add ArchLinux package (.tar.zst) (#185)
* 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
2026-01-26 14:14:26 +08:00
Fazri Gading
c4a32ce1e0 Release v2.1.1: Fix EPERM cross-platform error (#183)
* 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

* 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
2026-01-26 12:29:14 +08:00
AMIAY
7a9a67d8e8 Merge pull request #180 from amiayweb/develop
Release version v2.1.0
2026-01-25 23:24:19 +01:00
Fazri Gading
4c854953fe add support link on README 2026-01-26 06:02:35 +08:00
Fazri Gading
4cd0539ce3 Merge pull request #172 from Rahul-Sahani04/develop
feat: Add option to toggle hardware acceleration for launcher. Issue #170
2026-01-26 05:10:24 +08:00
Fazri Gading
fa2d451f90 Merge branch 'develop' into develop 2026-01-26 05:09:36 +08:00
Fazri Gading
a4faa7138c Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop 2026-01-26 05:05:31 +08:00
Fazri Gading
d285dc7517 fix: async-await for toggle and cleanup discordRPC 2026-01-26 05:05:25 +08:00
Fazri Gading
ceadd69eea Update release.yml: changed heads ref 2026-01-26 05:03:31 +08:00
Fazri Gading
6f0dd27c1d Update README.md header 2026-01-26 04:59:41 +08:00
Fazri Gading
ba95187ee6 fix: err_bad_request code 416 due to file size matched remote size, updated timeout to 15mins 2026-01-26 04:58:02 +08:00
AMIAY
9e54e07b22 R2 cdn added 2026-01-25 21:26:46 +01:00
AMIAY
a8e7e57c86 Merge pull request #178 from fazrigading/develop
fix: discordRPC error due to incorrect type value, update dotenv in package-lock
2026-01-25 21:01:53 +01:00
Terromur
d1ab58d51b Merge pull request #177 from amiayweb/fix-icon
Fix icon
2026-01-26 00:30:09 +05:00
Fazri Gading
8781025df9 chore: update readme.md, todo changelog 2026-01-26 03:28:30 +08:00
Terromur
81c52e9507 Fix icon 2026-01-26 00:28:09 +05:00
Fazri Gading
45314620e4 fix: Discord ID int to str, duplicate run of cleanupDiscordRPC function, and dismiss setTimeout on discordRPC destroy 2026-01-26 02:16:33 +08:00
Fazri Gading
43d5d20351 chore: disable patcher log to reduce logging length 2026-01-26 01:52:54 +08:00
Fazri Gading
72b4e0cba8 Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop 2026-01-26 00:27:25 +08:00
AMIAY
25d5131a7b Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop 2026-01-25 17:02:06 +01:00
AMIAY
ad3c73563d temp jar patcher 2026-01-25 17:02:02 +01:00
Rahul-Sahani04
f0f19f690f feat: Add option to toggle hardware acceleration for launcher #170 2026-01-25 21:08:47 +05:30
Fazri Gading
b27860a655 fix: adds back dotenv in package-lock.json 2026-01-25 23:25:33 +08:00
Fazri Gading
9788d0e496 Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop 2026-01-25 22:38:10 +08:00
Terromur
2a5780c2d4 Fix icon 2026-01-25 18:37:57 +05:00
AMIAY
8263b3f99b update pkgbuild 2026-01-25 14:37:00 +01:00
AMIAY
db56ef1624 onUpdateError fix 2026-01-25 14:31:17 +01:00
AMIAY
35f900d6ab Update fileManager.js 2026-01-25 14:15:19 +01:00
AMIAY
e1d1383ab7 Update package.json 2026-01-25 14:02:58 +01:00
AMIAY
8326deddb1 Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop 2026-01-25 13:31:38 +01:00
AMIAY
b11b78f7dc trying 2026-01-25 13:31:08 +01:00
Fazri Gading
62a2d76e4a Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop 2026-01-25 20:22:49 +08:00
Terromur
0ca8b4e02f Deleting garbage envs 2026-01-25 17:17:36 +05:00
Fazri Gading
c6a9d0ae07 merge last two commits to develop (#165)
* Add correct auto-detect version and commit

If a person uses PKGBUILD, it will automatically determine the latest version and commit.

* Remove maintainer and change to npm ci

---------

Co-authored-by: Terromur <79866197+Terromur@users.noreply.github.com>
2026-01-25 20:03:54 +08:00
Terromur
f438d6c8e0 Update PKGBUILD
Set png file from GUI/icon.png to 256x256 resolution for compatibility support.
2026-01-25 16:41:48 +05:00
Fazri Gading
f07e4a2004 Merge pull request #166 from amiayweb/main
merge last two commits from main
2026-01-25 18:37:47 +08:00
Fazri Gading
131580d3ba merge last two commits to develop (#165)
* Add correct auto-detect version and commit

If a person uses PKGBUILD, it will automatically determine the latest version and commit.

* Remove maintainer and change to npm ci

---------

Co-authored-by: Terromur <79866197+Terromur@users.noreply.github.com>
2026-01-25 18:36:40 +08:00
Fazri Gading
084347db03 prepare release for v2.1.0 (#164)
* fix: update tar to 7.5.6

* test: release on main branch using tag

* chore: remove previous release branch part

* fix: add deps for bsdtar

* fix: fix build tar.zst for arch

* fix: missing npm ci on release yml

* fix: remove pacman package json

* fix: revert tar version

* fix: revert tar in package-lock.json

* Update release.yml
2026-01-25 18:35:45 +08:00
Fazri Gading
589c5b457f Update release.yml 2026-01-25 18:34:01 +08:00
Fazri Gading
790d4d3f29 fix: revert tar in package-lock.json 2026-01-25 18:07:34 +08:00
Fazri Gading
52313910dc fix: revert tar version 2026-01-25 18:05:43 +08:00
Fazri Gading
a3f4d8e9d8 fix: remove pacman package json 2026-01-25 18:01:45 +08:00
Fazri Gading
86d617a4d3 fix: missing npm ci on release yml 2026-01-25 17:40:26 +08:00
Fazri Gading
0a97ac95fc fix: fix build tar.zst for arch 2026-01-25 17:27:55 +08:00
Fazri Gading
b94b45681b fix: add deps for bsdtar 2026-01-25 17:14:34 +08:00
Terromur
4086612e9d Remove maintainer and change to npm ci 2026-01-25 14:12:20 +05:00
Terromur
e7fca5a4c7 Add correct auto-detect version and commit
If a person uses PKGBUILD, it will automatically determine the latest version and commit.
2026-01-25 13:52:00 +05:00
Fazri Gading
e7bd20a1ec chore: remove previous release branch part 2026-01-25 16:50:28 +08:00
Fazri Gading
151b017653 test: release on main branch using tag 2026-01-25 16:46:26 +08:00
Fazri Gading
da3e14c434 fix: update tar to 7.5.6 2026-01-25 16:45:39 +08:00
Fazri Gading
6302734eeb fix: add missing icons for all platforms 2026-01-25 16:14:21 +08:00
Fazri Gading
07191860be chore: more detailed gitignore 2026-01-25 16:13:27 +08:00
Fazri Gading
2f767f191e chore: delete unused get-env-var functions 2026-01-25 15:31:29 +08:00
Fazri Gading
de9c7d81f5 fix: replace pacman build with pkg.tar.zst and remove its deps, changed CF and Discord key mode 2026-01-25 14:45:45 +08:00
Fazri Gading
4c3277392e merge branch 'main' (lost 4 commits) into develop 2026-01-25 13:54:15 +08:00
sanasol
f287cb55b9 Merge remote-tracking branch 'origin/develop' into develop 2026-01-25 01:27:29 +01:00
sanasol
d87db04653 feat(patcher): Implement DualAuth patcher with enhanced server patching
- Introduce DualAuthPatcher with support for hybrid authentication
- Update default auth domain to `auth.sanasol.ws`
- Integrate Java detection and bundled JRE handling for patcher execution
- Add server patch flag for avoiding redundant patching
- Automate DualAuthPatcher setup: download, compile, and execute with dependencies
- Enhance patching logic for extended logging and modularity
2026-01-25 01:27:19 +01:00
AMIAY
67aa41aefe fix 2026-01-25 01:03:49 +01:00
AMIAY
bd1dd146a9 Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop 2026-01-25 00:19:14 +01:00
AMIAY
c8d7707b70 need test - electron updater 2026-01-25 00:19:11 +01:00
xSamiVS
127c38f98b Update Spanish locale, add missing CurseForge API Key translation, implement Turkish translation, and fix contributor links comma. (#135)
* Update Spanish locale and add missing CurseForge API Key translation

- Updated the Spanish locale name to distinguish between multiple locale types.
- Added missing translation for the page indicating the missing CurseForge API Key.

* Implemented Turkish locale support

* Add Turkish locale to available languages

* Add missing comma in contributor links

* Correct Portuguese language name in available languages

---------

Co-authored-by: Fazri Gading <fazrigading@gmail.com>
2026-01-25 06:01:42 +08:00
AMIAY
f974d9c767 Update package-lock.json 2026-01-24 22:33:18 +01:00
Fazri Gading
7e4a45e466 Merge branch 'release' into develop 2026-01-25 05:24:08 +08:00
Fazri Gading
ea21fb15d6 fix: JRE retry button 2026-01-25 05:18:22 +08:00
sanasol
3d54cea9e7 feat(patcher): Support variable-length domains (4-16 chars)
- Add support for domains from 4 to 16 characters
- Domains <= 10 chars: direct replacement, subdomains stripped
- Domains 11-16 chars: split mode (first 6 chars -> subdomain prefix)
- Add length-prefixed byte format encoding for client binary
- Verify binary contents when checking if already patched
- Detect file updates and archive old backups with timestamps
- Fallback to legacy UTF-16LE format for older binaries
- Update patcher version to 2.0.0

Based on patching approach from Mollomm1/Hytale-EMULATOR

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 21:11:20 +01:00
AMIAY
9f43a32779 fix hardcoded branch + pre-release/release issue 2026-01-24 19:01:34 +01:00
Fazri Gading
9c8a12f25c fix: lastProgressTime variable init 2026-01-25 01:58:54 +08:00
Fazri Gading
a7d0523186 feat: auto-resume download process & auto-retry if disconnected (#143) 2026-01-25 01:36:20 +08:00
AMIAY
a6f716c61b removed getInstalledClientVersion 2026-01-24 13:44:56 +01:00
AMIAY
ca8ed171d1 removed overlay installation 2026-01-24 13:32:36 +01:00
AMIAY
679799c074 fix installation branch 2026-01-24 12:33:42 +01:00
AMIAY
87b168dd4c fix 2026-01-24 12:22:15 +01:00
AMIAY
679f065e24 delete updateManager 2026-01-24 12:09:54 +01:00
AMIAY
ecae7d2ee5 update 2026-01-24 12:06:45 +01:00
AMIAY
fa50fec34d Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop 2026-01-24 12:05:24 +01:00
AMIAY
c900129c1f fix patch 2026-01-24 12:05:10 +01:00
AMIAY
6b75858515 Delete .env.example 2026-01-24 12:03:07 +01:00
AMIAY
61bcdf9413 curseforge api 2026-01-24 12:01:37 +01:00
AMIAY
411d7d8aaf fix 2026-01-24 12:00:01 +01:00
Fazri Gading
8a87c7c4d9 docs: add and adjust more info on readme 2026-01-24 16:35:42 +08:00
Fazri Gading
34f93e962b docs: adjusted github template & add new contributors name (#133)
* docs: add new contributors to the list

* docs: fix template and new adjustments

* docs: fix github template & add new contributors

* removed config.yml
2026-01-24 15:57:20 +08:00
AMIAY
d8393543df fixing 2026-01-24 02:49:21 +01:00
Terromur
b62ffc126e Update PKGBUILD 2026-01-24 05:44:51 +05:00
AMIAY
3579d82776 fix (to try) 2026-01-24 01:41:09 +01:00
Terromur
b5c6c38d92 Update Hytale-F2P.desktop 2026-01-24 05:05:02 +05:00
Terromur
f932462578 Update PKGBUILD 2026-01-24 05:04:43 +05:00
Fazri Gading
e005b4293b docs: add footnotes and fixes 2026-01-24 04:10:54 +08:00
Fazri Gading
e43897f816 Draft Enhancement & Documentation for README.md
Needs some work on few TODO. Contributors PR are welcome.
2026-01-24 03:21:25 +08:00
AMIAY
3983fdb1bc pre-release & release game version [to check] 2026-01-23 17:54:57 +01:00
Fazri Gading
b46ce93af7 Release Stable Build v2.0.11 (#119)
* Add electron-updater auto-update support

- Install electron-updater package
- Configure GitHub releases publish settings
- Create AppUpdater class with full update lifecycle
- Integrate auto-update into main.js
- Add comprehensive documentation (AUTO-UPDATES.md, TESTING-UPDATES.md)
- Set up dev-app-update.yml for testing

* Add cache clearing documentation for electron-updater

- Introduced CLEAR-UPDATE-CACHE.md to guide users on clearing the electron-updater cache across macOS, Windows, and Linux.
- Added programmatic method for cache clearing in JavaScript.
- Enhanced update handling in main.js and preload.js to support new update events.
- Updated GUI styles for download buttons and progress indicators in update.js and style.css.

* Update auto-update UI and configuration

- Fix version display (newVersion field)
- Add download progress bar with real-time updates
- Reorder buttons: Install & Restart (primary), Manually Download (secondary)
- Update dev-app-update.yml to point to fork
- Update package.json version to 2.0.2

* Add installation effects and draggable progress bar

Introduces animated installation effects overlay and makes the progress bar draggable. Adds maximize window support, improves window controls styling, and enforces a single app instance. Removes the unused Skins page and related translations. Refines  various UI details for a more polished user experience.

* Adjust news card aspect ratio and add Play tab style

Set a default aspect ratio for .news-card and add a specific style for the LATEST NEWS section in the Play tab to override the aspect ratio and use full height.

* Add splash screen to launcher startup

Introduced a new splash screen (splash.html) and updated main.js to display it on startup before loading the main window. The splash screen is shown for 2.5 seconds as a placeholder for future loading logic, improving user experience during application launch.

* Display launcher version in UI

Adds a version display element to the bottom right of the UI, fetching the version from package.json via a new IPC handler. Updates main.js, preload.js, and ui.js to support retrieving and displaying the version, and adds relevant styles in style.css.

* Custom Mod loading fix (#92)

* feat: Add Repair Game functionality including UserData backup and cache clearing

* feat: Add In-App Logs Viewer and Logs Folder shortcut

* feat: Add Open Logs feature

* disable dev tools

* Fix Settings UI

* Implement custom mod loading, autoimport, auto repair

* Fixed Custom Mod loading issues and merge issues

* feat: Externalize sensitive API keys and Discord client ID into environment variables using dotenv.

* feat(mods): add profile-based mod management and auto-repair

* feat: add 'Close launcher on game start' option and improve app termination behavior (#93)

* update main branch to release/v2.0.2b (#86)

* add more linux pkgs, create auto-release and pre-release feature for Github Actions

* removed package-lock from gitignore

* update .gitignore for local build

* add package-lock.json to maintain stability development

* update version to 2.0.2b also add deps for rpm and arch

* update 2.0.2b: add arm64 support, product and executable name, maintainers; remove snap;

* update 2.0.2b: add latest.yml for win & linux, arm64 support; remove snap

* fix release build naming

* Prepare release v2.0.2b

* feat: add 'Close launcher on game start' option and improve app termination behavior

- Added 'Close launcher on game start' setting in GUI and backend.
- Implemented automatic app quit after game launch if setting is enabled.
- Added Cmd+Q (Mac) and Ctrl+Q/Alt+F4 (Win/Linux) shortcuts to quit the app.
- Updated 'window-close' handler to fully quit the app instead of just closing the window.
- Added i18n support for the new setting in English, Spanish, and Portuguese.

---------

Co-authored-by: Fazri Gading <fazrigading@gmail.com>
Co-authored-by: Arnav Singh <hi.arnavsingh3@gmail.com>

* Update publish config to point to chasem-dev fork

* Fix Linux metadata files in workflow and improve error handling

* Bump version to 2.0.5

* Bump version to 2.0.6

* Fix update popup showing for same version - add version comparison checks

* Bump version to 2.0.7

* Fix SHA512 checksum mismatch handling - clear cache and retry automatically

* Bump version to 2.0.8

* Bump version to 2.0.9

* Fix: Use explicit latest-linux.yml to prevent yml file collision

The glob pattern latest*.yml was matching both latest-linux.yml AND
latest.yml from the Linux build, causing the Windows latest.yml to be
overwritten with incorrect checksums.

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

* Bump version to 2.0.10

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

* Fix: Remove portable target to fix SHA512 checksum mismatch

The portable and nsis targets both produced x64.exe files with the same
name, causing one to overwrite the other. The latest.yml contained the
checksum from one build while the actual file was from the other build.

Removed portable target - nsis installer is sufficient.
Bump version to 2.0.11

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

* Remove outdated documentation files related to auto-updates, build instructions, and testing updates. Update `dev-app-update.yml` and `package.json` to reflect the correct GitHub owner. This cleanup streamlines the project and ensures accurate configuration for future updates.

* Add semantic versioning policy documentation - numerical versions only

* Update package-lock.json to include new dependencies and versions, enhancing project stability and compatibility.

* fixed imgur restriction for UK

* fix: adds EGL env var to detect installed NVIDIA GPU

* Update release.yml

* patch v2.0.11-beta: fix env issue in GA release, warn Intel Mac users, add com templates. (#115)

* fix: throw error for Intel Mac user
* docs: first draft of issue and PR template
* fix: env of curseforge API key and discord client ID

* implemented late patch should be in #115

* Final patch for release.yml v2.0.11

---------

Co-authored-by: chasem-dev <myers.a.chase@gmail.com>
Co-authored-by: AMIAY <letudiantenrap.collab@gmail.com>
Co-authored-by: Rahul Sahani <110347707+Rahul-Sahani04@users.noreply.github.com>
Co-authored-by: Arnav Singh <72737311+ArnavSingh77@users.noreply.github.com>
Co-authored-by: Arnav Singh <hi.arnavsingh3@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-24 00:07:59 +08:00
Fazri Gading
2a87acfe46 Final patch for release.yml v2.0.11 2026-01-23 22:46:31 +08:00
Fazri Gading
a2e2d5e5fd implemented late patch should be in #115 2026-01-23 22:10:35 +08:00
Fazri Gading
34143d9872 patch v2.0.11-beta: fix env issue in GA release, warn Intel Mac users, add com templates. (#115)
* fix: throw error for Intel Mac user
* docs: first draft of issue and PR template
* fix: env of curseforge API key and discord client ID
2026-01-23 21:30:27 +08:00
Fazri Gading
08c2218cf8 Merge branch 'feature/community-templates' into develop 2026-01-23 21:14:20 +08:00
Fazri Gading
032418b7f7 Merge branch 'fix/x86-mac-pwr-warning' into develop 2026-01-23 21:11:46 +08:00
Fazri Gading
fc05725a43 Merge pull request #114 from amiayweb/env-test
Merge v2.0.11-beta Build 2: env test & readme and server guide fix
2026-01-23 16:32:42 +08:00
Fazri Gading
203a56879f Update release.yml 2026-01-23 16:20:40 +08:00
Fazri Gading
7a0065ea2b Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop 2026-01-23 14:48:23 +08:00
Fazri Gading
ac08eb50ff Merge #112: update README.md and merge PR #105
Update README.md and merge PR #105 to develop branch
2026-01-23 14:37:06 +08:00
Fazri Gading
70fe4203ef Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop 2026-01-23 13:11:43 +08:00
Fazri Gading
f433120084 fix: adds EGL env var to detect installed NVIDIA GPU 2026-01-23 13:11:19 +08:00
AMIAY
f4099acbed Merge pull request #104 from chasem-dev/v2.0.11
V2.0.11 - Auto Updater
2026-01-23 02:04:15 +01:00
Fazri Gading
da843257c1 docs: first draft of issue and PR template 2026-01-23 06:00:36 +08:00
Fazri Gading
e4576042be fix: throw error for Intel Mac user 2026-01-23 05:57:21 +08:00
AMIAY
71974e031f Merge pull request #105 from gaurav-null/readme-tailscale-multiplayer
updated SERVER.md to add platform specification on radmin and add Tai…
2026-01-22 22:45:48 +01:00
guarav-null
5bd52f09db updated SERVER.md to add platform specification on radmin and add Tailscale guide for max compability 2026-01-23 03:05:29 +05:30
AMIAY
1ba6b22b74 fixed imgur restriction for UK 2026-01-22 20:40:28 +01:00
chasem-dev
a1bc88b754 Update package-lock.json to include new dependencies and versions, enhancing project stability and compatibility. 2026-01-22 14:07:04 -05:00
chasem-dev
24c2371b50 Add semantic versioning policy documentation - numerical versions only 2026-01-22 13:10:01 -05:00
chasem-dev
4c6e1a616e Merge upstream/develop into v2.0.11 - sync with main repository 2026-01-22 13:07:34 -05:00
chasem-dev
b54eb4e834 Remove outdated documentation files related to auto-updates, build instructions, and testing updates. Update dev-app-update.yml and package.json to reflect the correct GitHub owner. This cleanup streamlines the project and ensures accurate configuration for future updates. 2026-01-22 13:05:34 -05:00
chasem-dev
a1c74e4175 Fix: Remove portable target to fix SHA512 checksum mismatch
The portable and nsis targets both produced x64.exe files with the same
name, causing one to overwrite the other. The latest.yml contained the
checksum from one build while the actual file was from the other build.

Removed portable target - nsis installer is sufficient.
Bump version to 2.0.11

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-22 12:50:10 -05:00
chasem-dev
260e6c1126 Bump version to 2.0.10
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-22 11:53:34 -05:00
chasem-dev
6eb628559b Fix: Use explicit latest-linux.yml to prevent yml file collision
The glob pattern latest*.yml was matching both latest-linux.yml AND
latest.yml from the Linux build, causing the Windows latest.yml to be
overwritten with incorrect checksums.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-22 11:53:15 -05:00
chasem-dev
052b5dc7dc Bump version to 2.0.9 2026-01-22 11:30:59 -05:00
chasem-dev
7e9b5046df Bump version to 2.0.8 2026-01-22 11:13:01 -05:00
chasem-dev
204d6b21f6 Fix SHA512 checksum mismatch handling - clear cache and retry automatically 2026-01-22 11:12:59 -05:00
chasem-dev
740d516cfe Bump version to 2.0.7 2026-01-22 10:36:19 -05:00
chasem-dev
ce052add0d Fix update popup showing for same version - add version comparison checks 2026-01-22 10:36:18 -05:00
chasem-dev
d7a904c641 Bump version to 2.0.6 2026-01-22 10:18:33 -05:00
chasem-dev
d5d2f60c97 Bump version to 2.0.5 2026-01-22 10:18:30 -05:00
chasem-dev
61433bfeea Fix Linux metadata files in workflow and improve error handling 2026-01-22 10:18:28 -05:00
chasem-dev
9eb5d1759c Update publish config to point to chasem-dev fork 2026-01-22 09:11:10 -05:00
Arnav Singh
68d697576a feat: add 'Close launcher on game start' option and improve app termination behavior (#93)
* update main branch to release/v2.0.2b (#86)

* add more linux pkgs, create auto-release and pre-release feature for Github Actions

* removed package-lock from gitignore

* update .gitignore for local build

* add package-lock.json to maintain stability development

* update version to 2.0.2b also add deps for rpm and arch

* update 2.0.2b: add arm64 support, product and executable name, maintainers; remove snap;

* update 2.0.2b: add latest.yml for win & linux, arm64 support; remove snap

* fix release build naming

* Prepare release v2.0.2b

* feat: add 'Close launcher on game start' option and improve app termination behavior

- Added 'Close launcher on game start' setting in GUI and backend.
- Implemented automatic app quit after game launch if setting is enabled.
- Added Cmd+Q (Mac) and Ctrl+Q/Alt+F4 (Win/Linux) shortcuts to quit the app.
- Updated 'window-close' handler to fully quit the app instead of just closing the window.
- Added i18n support for the new setting in English, Spanish, and Portuguese.

---------

Co-authored-by: Fazri Gading <fazrigading@gmail.com>
Co-authored-by: Arnav Singh <hi.arnavsingh3@gmail.com>
2026-01-22 18:11:16 +08:00
Rahul Sahani
a8da559e93 Custom Mod loading fix (#92)
* feat: Add Repair Game functionality including UserData backup and cache clearing

* feat: Add In-App Logs Viewer and Logs Folder shortcut

* feat: Add Open Logs feature

* disable dev tools

* Fix Settings UI

* Implement custom mod loading, autoimport, auto repair

* Fixed Custom Mod loading issues and merge issues

* feat: Externalize sensitive API keys and Discord client ID into environment variables using dotenv.

* feat(mods): add profile-based mod management and auto-repair
2026-01-22 18:01:57 +08:00
AMIAY
828d05ca33 Update README.md 2026-01-22 10:46:09 +01:00
AMIAY
75f9403888 Display launcher version in UI
Adds a version display element to the bottom right of the UI, fetching the version from package.json via a new IPC handler. Updates main.js, preload.js, and ui.js to support retrieving and displaying the version, and adds relevant styles in style.css.
2026-01-22 08:07:32 +01:00
AMIAY
b61c94d348 Add splash screen to launcher startup
Introduced a new splash screen (splash.html) and updated main.js to display it on startup before loading the main window. The splash screen is shown for 2.5 seconds as a placeholder for future loading logic, improving user experience during application launch.
2026-01-22 07:59:27 +01:00
AMIAY
c0109575d6 Adjust news card aspect ratio and add Play tab style
Set a default aspect ratio for .news-card and add a specific style for the LATEST NEWS section in the Play tab to override the aspect ratio and use full height.
2026-01-22 07:43:39 +01:00
AMIAY
2a024b61dd Add installation effects and draggable progress bar
Introduces animated installation effects overlay and makes the progress bar draggable. Adds maximize window support, improves window controls styling, and enforces a single app instance. Removes the unused Skins page and related translations. Refines  various UI details for a more polished user experience.
2026-01-22 07:41:35 +01:00
chasem-dev
1c39e8e4c6 Update auto-update UI and configuration
- Fix version display (newVersion field)
- Add download progress bar with real-time updates
- Reorder buttons: Install & Restart (primary), Manually Download (secondary)
- Update dev-app-update.yml to point to fork
- Update package.json version to 2.0.2
2026-01-22 00:26:46 -05:00
chasem-dev
753bd4fd61 Add cache clearing documentation for electron-updater
- Introduced CLEAR-UPDATE-CACHE.md to guide users on clearing the electron-updater cache across macOS, Windows, and Linux.
- Added programmatic method for cache clearing in JavaScript.
- Enhanced update handling in main.js and preload.js to support new update events.
- Updated GUI styles for download buttons and progress indicators in update.js and style.css.
2026-01-22 00:26:01 -05:00
chasem-dev
cefb4c5575 Add electron-updater auto-update support
- Install electron-updater package
- Configure GitHub releases publish settings
- Create AppUpdater class with full update lifecycle
- Integrate auto-update into main.js
- Add comprehensive documentation (AUTO-UPDATES.md, TESTING-UPDATES.md)
- Set up dev-app-update.yml for testing
2026-01-22 00:03:02 -05:00
Fazri Gading
1c779e0e41 fix v2.0.2b changelog in README.md file 2026-01-22 02:18:17 +08:00
Fazri Gading
bb474fe233 Update README.md v2.0.2b changelog 2026-01-22 02:17:41 +08:00
Fazri Gading
917f5f455b Merge branch 'develop' 2026-01-22 02:15:36 +08:00
Fazri Gading
1dd42bdc79 Merge pull request #88 from amiayweb/release
merge v2.0.2b release files to main branch
2026-01-22 02:01:01 +08:00
Fazri Gading
7cfe3edd32 Merge branch 'main' into release 2026-01-22 01:44:53 +08:00
Fazri Gading
eb22758ab9 adds v2.0.2b changelog 2026-01-22 00:53:10 +08:00
Fazri Gading
42fd51486a feat: auto-detect GPU for Windows and MacOS (#87) 2026-01-21 22:13:47 +08:00
xSamiVS
9ef05e8322 Added internationalization support (i18n) (#74)
* - Implemented i18n.
- Updated UI elements to use localized strings for various messages and confirmations.
- Added language selection functionality in settings with appropriate event handling.
- Created English localization file with translations for all new strings.
- Updated backend to save and load user-selected language preferences.

* Add Spanish localization for the GUI

* Add Portuguese (Brazil) localization for the GUI

* update main branch to release/v2.0.2b (#86)

* add more linux pkgs, create auto-release and pre-release feature for Github Actions

* removed package-lock from gitignore

* update .gitignore for local build

* add package-lock.json to maintain stability development

* update version to 2.0.2b also add deps for rpm and arch

* update 2.0.2b: add arm64 support, product and executable name, maintainers; remove snap;

* update 2.0.2b: add latest.yml for win & linux, arm64 support; remove snap

* fix release build naming

* Prepare release v2.0.2b

* Update localization for game repair and GPU settings

Added new localization entries for game repair and GPU preferences.

* Update spanish localization for game repair and GPU settings

* Update portuguese (brazil) for game repair and GPU settings

* Update localization for system logs in English, Spanish, and Portuguese

---------

Co-authored-by: Fazri Gading <fazrigading@gmail.com>
2026-01-21 21:41:12 +08:00
Fazri Gading
4ac12e0e24 fix release version name 2026-01-21 21:01:32 +08:00
AMIAY
72a151930e Patch Discord invite URLs in client binary 2026-01-21 13:30:49 +01:00
Fazri Gading
a9644b8c64 remove v2 suffix from name and set consistent artifact name 2026-01-21 20:12:14 +08:00
Fazri Gading
9fc238e103 update executable name to be consistent with product name 2026-01-21 19:53:23 +08:00
Fazri Gading
b62e94a415 fix: package.json Module Not Found in 'Get version' step 2026-01-21 19:53:00 +08:00
Fazri Gading
3e82e8fadb add libarchive-tools for bsdtar in release workflow 2026-01-21 18:38:49 +08:00
Fazri Gading
a355133ccf update main branch to release/v2.0.2b (#86)
* add more linux pkgs, create auto-release and pre-release feature for Github Actions

* removed package-lock from gitignore

* update .gitignore for local build

* add package-lock.json to maintain stability development

* update version to 2.0.2b also add deps for rpm and arch

* update 2.0.2b: add arm64 support, product and executable name, maintainers; remove snap;

* update 2.0.2b: add latest.yml for win & linux, arm64 support; remove snap

* fix release build naming

* Prepare release v2.0.2b
2026-01-21 16:39:18 +08:00
Fazri Gading
ff5acb5278 fix release build naming 2026-01-21 16:24:47 +08:00
Fazri Gading
5d75ca80aa update 2.0.2b: add latest.yml for win & linux, arm64 support; remove snap 2026-01-21 16:07:57 +08:00
Fazri Gading
281aa6fcde update 2.0.2b: add arm64 support, product and executable name, maintainers; remove snap; 2026-01-21 16:07:12 +08:00
Fazri Gading
158d0af820 update version to 2.0.2b also add deps for rpm and arch 2026-01-21 16:05:20 +08:00
Fazri Gading
503f304704 add package-lock.json to maintain stability development 2026-01-21 16:04:40 +08:00
Fazri Gading
234e1e1008 update .gitignore for local build 2026-01-21 16:04:17 +08:00
Fazri Gading
d002e831cd removed package-lock from gitignore 2026-01-21 07:42:03 +08:00
Fazri Gading
048f2040f1 add more linux pkgs, create auto-release and pre-release feature for Github Actions 2026-01-21 06:41:25 +08:00
Rahul Sahani
b05aeef66d feat: Add Repair Game button, UserData backup and cache clearing (#79)
* feat: Add Repair Game functionality including UserData backup and cache clearing

* feat: Add In-App Logs Viewer and Logs Folder shortcut

* feat: Add Open Logs feature

* disable dev tools

* Fix Settings UI

* fix reorder settings section in index.html

relocated sections in settings from most used to least:
1. game options (playername, opengamedir, repair, GPUpreference)
2. player uuid management
3. discord integration rich presence
4. custom java path

---------

Co-authored-by: Fazri Gading <super.fai700@gmail.com>
2026-01-21 06:27:33 +08:00
Fazri Gading
30265549cf feat: Implement GPU preference system with auto-detection and UI enhancements (#82)
* feat: add GPU options for launcher

- Add GPU preference setting (Auto/Integrated/Dedicated)
- Implement Linux GPU selection with DRI_PRIME and NVIDIA env vars
- Add GPU detection using Electron's app.getGPUInfo()
- Update settings UI with GPU preference dropdown
- Integrate GPU preference into game launch process

* feat: detailed GPU info in auto-detection feature on startup

* add gpudetection feature to launcher backend

* feat: detailed GPU info in auto-detection feature on startup

* feat: auto-detect dedicated GPU on hybrid laptops (iGPU+dGPU)

* add fallbacks to and option to use integrated GPU

* add package-lock and fix deps version

* changed 'Nvidia' string to 'NVIDIA'

* fix: selecting `dedicated` option while using nvidia GPU did not set its specific env variables

* remove unused `CONFIG_FILE` variable on launcher core modules

* fix: duplicated save-load gpu detection functions

* move game option settings to the top, while custom java to the bottom

* fix: settings-header margin-bottom from 3rem to 1rem and supress line-clamp warning
2026-01-21 05:54:17 +08:00
Terromur
5a3efba1d6 Update discription 2026-01-21 01:43:40 +05:00
Terromur
479f24e86f Update PKGBUILD 2026-01-21 01:28:53 +05:00
Fazri Gading
611b7a7084 Merge branch 'main' into main 2026-01-21 04:05:02 +08:00
Terromur
e472435927 Update PKGBUILD 2026-01-21 00:34:52 +05:00
AMIAY
f40d0105df Update package-lock.json 2026-01-20 18:35:26 +01:00
AMIAY
96db9adf68 Update package.json 2026-01-20 18:34:57 +01:00
Fazri Gading
a0f49f126c add libarchive-tools dep for pacman package 2026-01-21 00:53:47 +08:00
Fazri Gading
f8333c09cd Update package.json 2026-01-21 00:43:29 +08:00
Fazri Gading
bcf0326763 Merge branch 'main' of https://github.com/fazrigading/Hytale-F2P 2026-01-21 00:31:20 +08:00
Fazri Gading
0a5c3db710 add 3 package formats for Linux, descriptions, and update launcher version to 2.0.2 2026-01-21 00:27:11 +08:00
Fazri Gading
905a9d754c Merge GPU preference feature branch to main branch version 2.0.2 for testing (#3)
* modernized UI for GPU Preference option

* feat: auto-detect dedicated GPU on hybrid laptops (iGPU+dGPU)

* feat: detailed GPU info in auto-detection feature on startup

* feat: add GPU options for launcher

- Add GPU preference setting (Auto/Integrated/Dedicated)
- Implement Linux GPU selection with DRI_PRIME and NVIDIA env vars
- Add GPU detection using Electron's app.getGPUInfo()
- Update settings UI with GPU preference dropdown
- Integrate GPU preference into game launch process

* feat: auto-detect dedicated GPU on hybrid laptops (iGPU+dGPU)

* added fallbacks to and option to use integrated GPU.

* add package-lock and fix deps version

* changed 'Nvidia' string to 'NVIDIA'

* fix: selecting `dedicated` option while using nvidia GPU did not set its specific env variables

* remove unused `CONFIG_FILE` variable on launcher core modules

* fix: duplicated save-load gpu detection functions

* move game option settings to the top, while custom java to the bottom

* fix: settings-header margin-bottom from 3rem to 1rem and supress line-clamp warning
2026-01-20 23:45:38 +08:00
Fazri Gading
99f032e9ab Merge branch 'main' of https://github.com/amiayweb/Hytale-F2P 2026-01-20 19:37:54 +08:00
AMIAY
5e3506a9ac Update README.md 2026-01-20 09:57:21 +01:00
Fazri Gading
2671a59f38 Revert "add save-load GPU preference handler"
This reverts commit b957a76aede8f9425b0a283d1444a32f55baa95a.
2026-01-20 16:26:28 +08:00
Fazri Gading
8e9af9c768 add save-load GPU preference handler 2026-01-20 16:26:28 +08:00
47 changed files with 4 additions and 20522 deletions

61
.github/README1.md vendored
View File

@@ -1,61 +0,0 @@
# GitHub Actions
## Build and Release Workflow
The `release.yml` workflow automatically builds the launcher for all platforms.
### Triggers
| Trigger | Builds | Creates Release |
|---------|--------|-----------------|
| Push to `main` | Yes | No |
| Push tag `v*` | Yes | Yes |
| Manual dispatch | Yes | No |
### Platforms
All builds run in parallel:
- **Linux** (ubuntu-latest): AppImage, deb
- **Windows** (windows-latest): NSIS installer, portable exe
- **macOS** (macos-latest): Universal DMG (Intel + Apple Silicon)
### Creating a Release
1. Update version in `package.json`
2. Commit and push to `main`
3. Create and push a version tag:
```bash
git tag v2.0.1
git push origin v2.0.1
```
The workflow will:
1. Build all platforms in parallel
2. Upload artifacts to GitHub Release
3. Generate release notes automatically
### Build Artifacts
After each build, artifacts are available in the Actions tab for 90 days:
- `linux-builds`: `.AppImage`, `.deb`
- `windows-builds`: `.exe`
- `macos-builds`: `.dmg`, `.zip`, `latest-mac.yml`
### Local Development
Build locally for your platform:
```bash
npm run build:linux
npm run build:win
npm run build:mac
```
Or build all platforms (requires appropriate OS):
```bash
npm run build:all
```

View File

@@ -1,93 +0,0 @@
name: Build and Release
on:
push:
branches:
- main
tags:
- 'v*'
workflow_dispatch:
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npx electron-builder --linux --publish never
- uses: actions/upload-artifact@v4
with:
name: linux-builds
path: |
dist/*.AppImage
dist/*.deb
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npx electron-builder --win --publish never
- uses: actions/upload-artifact@v4
with:
name: windows-builds
path: |
dist/*.exe
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npx electron-builder --mac --publish never
- uses: actions/upload-artifact@v4
with:
name: macos-builds
path: |
dist/*.dmg
dist/*.zip
dist/latest-mac.yml
release:
needs: [build-linux, build-windows, build-macos]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Display structure of downloaded files
run: ls -R artifacts
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('manual-{0}', github.sha) }}
name: ${{ github.ref_type == 'tag' && github.ref_name || format('Manual build {0}', github.sha) }}
files: |
artifacts/linux-builds/**/*
artifacts/windows-builds/**/*
artifacts/macos-builds/**/*
generate_release_notes: true
draft: false
prerelease: false

4
.gitignore vendored
View File

@@ -1,4 +0,0 @@
dist/*
node_modules/*
package-lock.json
bun.lock

View File

@@ -1,39 +0,0 @@
# Build Instructions
## Prerequisites
Install dependencies:
```bash
npm install
```
## Building
### Build for current platform:
```bash
npm run build
```
### Build for specific platform:
**Windows:**
```bash
npm run build:win
```
**Linux:**
```bash
npm run build:linux
```
**macOS:**
```bash
npm run build:mac
```
### Build for all platforms:
```bash
npm run build:all
```
Built executables will be in the `dist/` directory

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -1,668 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hytale F2P Launcher</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body class="bg-black text-white overflow-hidden font-sans select-none" tabindex="-1">
<div class="absolute inset-0 z-0">
<img src="https://i.imgur.com/Visrk66.png" alt="Background" class="w-full h-full object-cover" />
<div class="absolute inset-0 bg-black/60"></div>
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg viewBox=" 0 0 256 256"
xmlns="http://www.w3.org/2000/svg" %3E%3Cfilter id="noiseFilter" %3E%3CfeTurbulence type="fractalNoise"
baseFrequency="0.65" numOctaves="3" stitchTiles="stitch" /%3E%3C/filter%3E%3Crect width="100%25"
height="100%25" filter="url(%23noiseFilter)" opacity="0.1" /%3E%3C/svg%3E')] opacity-20"></div>
</div>
<div class="flex w-full h-screen relative z-10">
<nav class="sidebar">
<div class="sidebar-logo">
<img src="./icon.png" alt="Hytale Logo" />
</div>
<div class="sidebar-nav">
<div class="nav-item active" data-page="play">
<i class="fas fa-play"></i>
<span class="nav-tooltip">Play</span>
</div>
<div class="nav-item" data-page="mods">
<i class="fas fa-box"></i>
<span class="nav-tooltip">Mods</span>
</div>
<div class="nav-item" data-page="news">
<i class="fas fa-newspaper"></i>
<span class="nav-tooltip">News</span>
</div>
<div class="nav-item" data-page="chat">
<i class="fas fa-comments"></i>
<span class="nav-tooltip">Players Chat</span>
</div>
<div class="nav-item" data-page="settings">
<i class="fas fa-cog"></i>
<span class="nav-tooltip">Settings</span>
</div>
<div class="nav-item" data-page="skins">
<i class="fas fa-user"></i>
<span class="nav-tooltip">Skins</span>
</div>
</div>
</nav>
<main class="main-content">
<header class="header">
<div id="playersOnlineCounter" class="players-counter">
<i class="fas fa-users"></i>
<span class="counter-label">Players:</span>
<span id="onlineCount" class="counter-value">0</span>
</div>
<div class="profile-selector" id="profileSelector">
<button class="profile-btn" onclick="toggleProfileDropdown()">
<i class="fas fa-user-circle"></i>
<span id="currentProfileName">Default</span>
<i class="fas fa-chevron-down"></i>
</button>
<div class="profile-dropdown" id="profileDropdown">
<div class="profile-list" id="profileList">
<!-- Profiles populated by JS -->
</div>
<div class="profile-divider"></div>
<div class="profile-action" onclick="openProfileManager()">
<i class="fas fa-cog"></i>
<span>Manage Profiles</span>
</div>
</div>
</div>
<div class="window-controls">
<button class="control-btn minimize" onclick="window.electronAPI?.minimizeWindow()">
<i class="fas fa-minus"></i>
</button>
<button class="control-btn close" onclick="window.electronAPI?.closeWindow()">
<i class="fas fa-times"></i>
</button>
</div>
</header>
<div class="game-title-section">
<h1 class="game-title">
HY<span class="title-accent">TALE</span>
</h1>
<div class="game-tags">
<span class="tag">FREE TO PLAY</span>
</div>
</div>
<div class="content-pages">
<div id="install-page" class="page install-page">
<div class="install-content">
<div class="install-header">
<h1 class="install-title">
HYTA<span class="title-accent">LE</span>
</h1>
<p class="install-subtitle">FREE TO PLAY LAUNCHER</p>
</div>
<div class="install-form">
<div class="form-group">
<label class="form-label">Player Name</label>
<input type="text" id="installPlayerName" placeholder="Enter your name"
class="form-input" value="Player" />
</div>
<div class="form-group">
<label class="checkbox-group">
<input type="checkbox" id="installCustomCheck" class="custom-checkbox">
<span class="checkbox-label">Custom Installation</span>
</label>
<div id="installCustomOptions" class="custom-options">
<div class="form-subgroup">
<label class="form-label">Installation Folder</label>
<div class="input-with-button">
<input type="text" id="installPath" placeholder="Default location"
class="form-input" readonly />
<button onclick="browseInstallPath()" class="browse-btn">
<i class="fas fa-folder-open"></i>
</button>
</div>
</div>
</div>
</div>
<button id="installBtn" class="install-button" onclick="installGame()">
<i class="fas fa-download mr-2"></i>
<span id="installText">INSTALL HYTALE</span>
</button>
</div>
</div>
</div>
<div id="launcher-container" class="launcher-container" style="display: none;">
<div id="play-page" class="page active">
<div class="play-section">
<div class="play-content">
<div class="play-header">
<h2 class="play-title">
<i class="fas fa-play-circle mr-2"></i>
READY TO PLAY
</h2>
<p class="play-subtitle">Launch Hytale and enter the adventure</p>
</div>
<button id="homePlayBtn" class="home-play-button" onclick="launch()">
<i class="fas fa-play"></i>
<span>PLAY HYTALE</span>
</button>
</div>
</div>
<div class="news-section">
<div class="news-header">
<h2 class="news-title">
<i class="fas fa-star mr-2"></i>
LATEST NEWS
</h2>
<button class="view-all-btn" onclick="navigateToPage('news')">
VIEW ALL <i class="fas fa-arrow-right ml-1"></i>
</button>
</div>
<div id="newsGrid" class="news-grid-horizontal"></div>
</div>
</div>
<div id="mods-page" class="page">
<div class="mods-header">
<div class="mods-search-container">
<i class="fas fa-search"></i>
<input type="text" id="modsSearch" placeholder="Search mods..." class="mods-search" />
</div>
<div class="mods-actions">
<button id="myModsBtn" class="mods-btn-primary">
<i class="fas fa-box"></i>
MY MODS
</button>
</div>
</div>
<div id="browseModsList" class="mods-browse-container">
</div>
<div class="mods-pagination">
<button id="prevPage" class="pagination-btn">
<i class="fas fa-chevron-left"></i>
PREVIOUS
</button>
<span class="pagination-info">
Page <span id="currentPage">1</span> of <span id="totalPages">1</span>
</span>
<button id="nextPage" class="pagination-btn">
NEXT
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
<div id="news-page" class="page">
<div class="news-header">
<h2 class="news-title">
<i class="fas fa-newspaper mr-2"></i>
ALL NEWS
</h2>
</div>
<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>
PLAYERS CHAT
</h2>
<div class="chat-header-actions">
<button id="chatColorBtn" class="chat-color-btn">
<i class="fas fa-palette"></i>
<span>Color</span>
</button>
<div class="chat-online-badge">
<i class="fas fa-circle"></i>
<span id="chatOnlineCount">0</span> online
</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"
placeholder="Type your message... (Links are automatically censored)" 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>
Secure chat - Links are censored
</span>
</div>
</div>
</div>
</div>
<div id="settings-page" class="page">
<div class="settings-container">
<div class="settings-header">
<h2 class="settings-title">
<i class="fas fa-cog mr-2"></i>
SETTINGS
</h2>
</div>
<div class="settings-content">
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-coffee"></i>
Java Runtime
</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">Use Custom Java Path</div>
<div class="checkbox-description">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">Java Executable Path</label>
<div class="settings-input-with-button">
<input type="text" id="customJavaPath" class="settings-input"
placeholder="Select Java path..." readonly />
<button id="browseJavaBtn" class="settings-browse-btn">
<i class="fas fa-folder-open"></i>
Browse
</button>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
Select the Java installation folder (supports Windows, Mac, Linux)
</p>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fab fa-discord"></i>
Discord Integration
</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">Enable Discord Rich Presence</div>
<div class="checkbox-description">Show your launcher activity on Discord
</div>
</div>
</label>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-gamepad"></i>
Game Options
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label">Player Name</label>
<input type="text" id="settingsPlayerName" class="settings-input"
placeholder="Enter your player name" maxlength="16" />
<p class="settings-hint">
<i class="fas fa-user"></i>
This name will be used in-game (1-16 characters)
</p>
</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">Open Game Location</div>
<div class="btn-description">Open the game installation folder</div>
</div>
</button>
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-fingerprint"></i>
Player UUID Management
</h3>
<div class="settings-option">
<div class="settings-input-group">
<label class="settings-input-label">Current UUID</label>
<div class="uuid-display-container">
<input type="text" id="currentUuid" class="settings-input uuid-input"
readonly placeholder="Loading UUID..." />
<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>
Your unique player identifier for this username
</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">Manage All UUIDs</div>
<div class="btn-description">View and manage all player UUIDs</div>
</div>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="skins-page" class="page">
<div class="placeholder-content">
<i class="fas fa-user text-6xl mb-4 text-purple-500"></i>
<h2>Skins</h2>
<p>Skin customization coming soon...</p>
</div>
</div>
</div>
</div>
</main>
</div>
<div id="myModsModal" class="mods-modal">
<div class="mods-modal-content">
<div class="mods-modal-header">
<h2 class="mods-modal-title">
<i class="fas fa-box mr-2"></i>
MY MODS
</h2>
<button id="closeMyModsModal" class="mods-modal-close">
<i class="fas fa-times"></i>
</button>
</div>
<div class="mods-modal-body">
<div id="installedModsList" class="installed-mods-list">
</div>
</div>
</div>
</div>
<div id="progressOverlay" class="progress-overlay" style="display: none;">
<div class="progress-content">
<div class="progress-info">
<span id="progressText">Initializing...</span>
<span id="progressPercent">0%</span>
</div>
<div class="progress-bar-container">
<div id="progressBarFill" class="progress-bar-fill"></div>
</div>
<div class="progress-details">
<span id="progressSpeed"></span>
<span id="progressSize"></span>
</div>
</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>
Join Chat
</h2>
</div>
<div class="chat-username-modal-body">
<p class="chat-username-modal-description">
Choose a username to join the Players Chat
</p>
<div class="chat-username-input-group">
<label for="chatUsernameInput" class="chat-username-label">Username</label>
<input type="text" id="chatUsernameInput" class="chat-username-input"
placeholder="Enter your username..." maxlength="20" autocomplete="off" />
<span class="chat-username-hint">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>
Cancel
</button>
<button id="chatUsernameSubmit" class="chat-username-btn-submit">
<i class="fas fa-check"></i>
Join Chat
</button>
</div>
</div>
</div>
<!-- UUID Management Modal -->
<div id="uuidModal" class="uuid-modal" style="display: none;">
<div class="uuid-modal-content">
<div class="uuid-modal-header">
<h2 class="uuid-modal-title">
<i class="fas fa-fingerprint mr-2"></i>
UUID Management
</h2>
<button id="uuidModalClose" class="modal-close-btn">
<i class="fas fa-times"></i>
</button>
</div>
<div class="uuid-modal-body">
<div class="uuid-current-section">
<h3 class="uuid-section-title">Current User UUID</h3>
<div class="uuid-current-display">
<input type="text" id="modalCurrentUuid" class="uuid-display-input" readonly />
<button id="modalCopyUuidBtn" class="uuid-action-btn copy-btn" title="Copy UUID">
<i class="fas fa-copy"></i>
</button>
<button id="modalRegenerateUuidBtn" class="uuid-action-btn regenerate-btn"
title="Generate New UUID">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="uuid-list-section">
<div class="uuid-list-header">
<h3 class="uuid-section-title">All Player UUIDs</h3>
<button id="generateNewUuidBtn" class="uuid-generate-btn">
<i class="fas fa-plus"></i>
Generate New UUID
</button>
</div>
<div id="uuidList" class="uuid-list">
<div class="uuid-loading">
<i class="fas fa-spinner fa-spin"></i>
Loading UUIDs...
</div>
</div>
</div>
<div class="uuid-custom-section">
<h3 class="uuid-section-title">Set Custom UUID</h3>
<div class="uuid-custom-form">
<input type="text" id="customUuidInput" class="uuid-input"
placeholder="Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
maxlength="36" />
<button id="setCustomUuidBtn" class="uuid-set-btn">
<i class="fas fa-check"></i>
Set UUID
</button>
</div>
<p class="uuid-custom-hint">
<i class="fas fa-exclamation-triangle"></i>
Warning: Setting a custom UUID will change your current player identity
</p>
</div>
</div>
</div>
</div>
<!-- Profile Manager Modal -->
<div id="profileManagerModal" class="profile-modal" style="display: none;">
<div class="profile-modal-content">
<div class="profile-modal-header">
<h2 class="profile-modal-title">
<i class="fas fa-users-cog mr-2"></i>
Manage Profiles
</h2>
<button class="modal-close-btn" onclick="closeProfileManager()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="profile-modal-body">
<div class="profile-manager-list" id="managerProfileList">
<!-- Populated by JS -->
</div>
<div class="profile-create-section">
<input type="text" id="newProfileName" placeholder="New Profile Name" class="profile-input"
maxlength="20">
<button class="profile-create-btn" onclick="createNewProfile()">
<i class="fas fa-plus"></i> Create Profile
</button>
</div>
</div>
</div>
</div>
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
<div class="flex items-center justify-center text-xs text-gray-400">
<span>Made by <a href="https://github.com/amiayweb" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@amiayweb</a> & <a
href="https://github.com/Relyz1993" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@Relyz</a></span>
<span class="mx-2">|</span>
<span>Contributors:
<a href="https://github.com/chasem-dev" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@chasem-dev</a>,
<a href="https://github.com/crimera" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@crimera</a>,
<a href="https://github.com/sanasol" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@sanasol</a>,
<a href="https://github.com/Terromur" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@terromur</a>,
<a href="https://github.com/ericiskoolbeans" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a>,
<a href="https://github.com/fazrigading" target="_blank"
class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading</a>
</span>
</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">Join our Discord community!</span>
<button class="notification-action"
onclick="window.electronAPI?.openExternal('https://discord.gg/n6HZ7NwSQd')">
Join Discord
</button>
</div>
<button class="notification-close" onclick="closeDiscordNotification()">
<i class="fas fa-times"></i>
</button>
</div>
<!-- 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>
Customize Username Color
</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>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">Custom color:</label>
<input type="color" id="customColor" value="#3498db">
</div>
</div>
<div class="color-preview">
<h4>Preview:</h4>
<div id="colorPreview" class="preview-username">YourUsername</div>
</div>
</div>
<div class="chat-color-modal-footer">
<button class="btn-secondary" onclick="closeChatColorModal()">Cancel</button>
<button class="btn-primary" onclick="applyChatColor()">Apply Color</button>
</div>
</div>
</div>
<script type="module" src="js/update.js"></script>
</body>
</html>

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()
};

View File

@@ -1,210 +0,0 @@
let isDownloading = false;
let installPage;
let installBtn;
let installText;
let installPlayerName;
let installCustomCheck;
let installCustomOptions;
let installPathInput;
export function setupInstallation() {
installPage = document.getElementById('install-page');
installBtn = document.getElementById('installBtn');
installText = document.getElementById('installText');
installPlayerName = document.getElementById('installPlayerName');
installCustomCheck = document.getElementById('installCustomCheck');
installCustomOptions = document.getElementById('installCustomOptions');
installPathInput = document.getElementById('installPath');
if (installCustomCheck && installCustomOptions) {
installCustomCheck.addEventListener('change', (e) => {
if (e.target.checked) {
installCustomOptions.classList.add('show');
} else {
installCustomOptions.classList.remove('show');
}
});
}
if (installPlayerName) {
installPlayerName.addEventListener('change', savePlayerName);
}
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
window.electronAPI.onProgressUpdate((data) => {
if (!isDownloading) return;
if (window.LauncherUI) {
window.LauncherUI.updateProgress(data);
}
});
}
}
export async function installGame() {
if (isDownloading || (installBtn && installBtn.disabled)) return;
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
const installPath = installPathInput ? installPathInput.value.trim() : '';
if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true;
if (installBtn) {
installBtn.disabled = true;
installText.textContent = 'INSTALLING...';
}
try {
if (window.electronAPI && window.electronAPI.installGame) {
const result = await window.electronAPI.installGame(playerName, '', installPath);
if (result.success) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true);
const playerNameInput = document.getElementById('playerName');
if (playerNameInput) playerNameInput.value = playerName;
resetInstallButton();
}, 2000);
}
} else {
throw new Error(result.error || 'Installation failed');
}
} else {
simulateInstallation(playerName);
}
} catch (error) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Installation failed: ${error.message}` });
setTimeout(() => {
window.LauncherUI.hideProgress();
resetInstallButton();
}, 3000);
}
}
}
function simulateInstallation(playerName) {
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 3;
if (progress > 100) progress = 100;
if (window.LauncherUI) {
window.LauncherUI.updateProgress({
percent: progress,
message: progress < 100 ? 'Installing game files...' : 'Installation complete!',
speed: 1024 * 1024 * (5 + Math.random() * 10),
downloaded: progress * 1024 * 1024 * 20,
total: 1024 * 1024 * 2000
});
}
if (progress >= 100) {
clearInterval(interval);
setTimeout(() => {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Installation completed successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(true);
const playerNameInput = document.getElementById('playerName');
if (playerNameInput) playerNameInput.value = playerName;
resetInstallButton();
}, 2000);
}
}, 1000);
}
}, 200);
}
function resetInstallButton() {
isDownloading = false;
if (installBtn) {
installBtn.disabled = false;
installText.textContent = 'INSTALL HYTALE';
}
}
export async function browseInstallPath() {
try {
if (window.electronAPI && window.electronAPI.selectInstallPath) {
const result = await window.electronAPI.selectInstallPath();
if (result && installPathInput) {
installPathInput.value = result;
}
}
} catch (error) {
console.error('Error browsing install path:', error);
}
}
async function savePlayerName() {
try {
if (window.electronAPI && window.electronAPI.saveSettings) {
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
await window.electronAPI.saveSettings({ playerName });
}
} catch (error) {
console.error('Error saving player name:', error);
}
}
export async function checkGameStatusAndShowInterface() {
try {
if (window.electronAPI && window.electronAPI.isGameInstalled) {
const installed = await window.electronAPI.isGameInstalled();
if (window.LauncherUI) {
window.LauncherUI.showLauncherOrInstall(installed);
}
if (installed) {
await loadPlayerSettings();
}
} else {
if (window.LauncherUI) {
window.LauncherUI.showLauncherOrInstall(false);
}
}
} catch (error) {
console.error('Error checking game status:', error);
if (window.LauncherUI) {
window.LauncherUI.showLauncherOrInstall(false);
}
}
}
async function loadPlayerSettings() {
try {
if (window.electronAPI && window.electronAPI.loadSettings) {
const settings = await window.electronAPI.loadSettings();
if (settings) {
const playerNameInput = document.getElementById('playerName');
const javaPathInput = document.getElementById('javaPath');
if (settings.playerName && playerNameInput) {
playerNameInput.value = settings.playerName;
}
if (settings.javaPath && javaPathInput) {
javaPathInput.value = settings.javaPath;
}
}
}
} catch (error) {
console.error('Error loading settings:', error);
}
}
window.installGame = installGame;
window.browseInstallPath = browseInstallPath;
document.addEventListener('DOMContentLoaded', async () => {
setupInstallation();
await checkGameStatusAndShowInterface();
});
window.browseInstallPath = browseInstallPath;
document.addEventListener('DOMContentLoaded', async () => {
setupInstallation();
await checkGameStatusAndShowInterface();
});

View File

@@ -1,532 +0,0 @@
let isDownloading = false;
let playBtn;
let playText;
let homePlayBtn;
let uninstallBtn;
let playerNameInput;
let javaPathInput;
export function setupLauncher() {
playBtn = document.getElementById('playBtn');
playText = document.getElementById('playText');
homePlayBtn = document.getElementById('homePlayBtn');
uninstallBtn = document.getElementById('uninstallBtn');
playerNameInput = document.getElementById('playerName');
javaPathInput = document.getElementById('javaPath');
if (playerNameInput) {
playerNameInput.addEventListener('change', savePlayerName);
}
if (javaPathInput) {
javaPathInput.addEventListener('change', saveJavaPath);
}
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
window.electronAPI.onProgressUpdate((data) => {
if (!isDownloading) return;
if (window.LauncherUI) {
window.LauncherUI.updateProgress(data);
}
});
}
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
window.electronAPI.onProgressUpdate((data) => {
if (!isDownloading) return;
if (window.LauncherUI) {
window.LauncherUI.updateProgress(data);
}
});
}
// Initial Profile Load
loadProfiles();
// Close dropdown on outside click
document.addEventListener('click', (e) => {
const selector = document.getElementById('profileSelector');
if (selector && !selector.contains(e.target)) {
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
}
});
}
// ==========================================
// PROFILE MANAGEMENT
// ==========================================
async function loadProfiles() {
try {
if (!window.electronAPI || !window.electronAPI.profile) return;
const profiles = await window.electronAPI.profile.list();
const activeProfile = await window.electronAPI.profile.getActive();
renderProfileList(profiles, activeProfile);
updateCurrentProfileUI(activeProfile);
} catch (error) {
console.error('Failed to load profiles:', error);
}
}
function renderProfileList(profiles, activeProfile) {
const list = document.getElementById('profileList');
const managerList = document.getElementById('managerProfileList');
if (!list) return;
// Dropdown List
list.innerHTML = profiles.map(p => `
<div class="profile-item ${p.id === activeProfile.id ? 'active' : ''}"
onclick="switchProfile('${p.id}')">
<span>${p.name}</span>
${p.id === activeProfile.id ? '<i class="fas fa-check ml-auto"></i>' : ''}
</div>
`).join('');
// Manager Modal List
if (managerList) {
managerList.innerHTML = profiles.map(p => `
<div class="profile-manager-item ${p.id === activeProfile.id ? 'active' : ''}">
<div class="flex items-center gap-3">
<i class="fas fa-user-circle text-xl text-gray-400"></i>
<div>
<div class="font-bold">${p.name}</div>
<div class="text-xs text-gray-500">ID: ${p.id.substring(0, 8)}...</div>
</div>
</div>
${p.id !== activeProfile.id ? `
<button class="profile-delete-btn" onclick="deleteProfile('${p.id}')" title="Delete Profile">
<i class="fas fa-trash"></i>
</button>
` : '<span class="text-xs text-green-500 font-bold px-2">ACTIVE</span>'}
</div>
`).join('');
}
}
function updateCurrentProfileUI(profile) {
const nameEl = document.getElementById('currentProfileName');
if (nameEl && profile) {
nameEl.textContent = profile.name;
}
}
window.toggleProfileDropdown = () => {
const dropdown = document.getElementById('profileDropdown');
if (dropdown) {
dropdown.classList.toggle('show');
}
};
window.openProfileManager = () => {
const modal = document.getElementById('profileManagerModal');
if (modal) {
modal.style.display = 'flex';
// Refresh list
loadProfiles();
}
// Close dropdown
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
};
window.closeProfileManager = () => {
const modal = document.getElementById('profileManagerModal');
if (modal) modal.style.display = 'none';
};
window.createNewProfile = async () => {
const input = document.getElementById('newProfileName');
if (!input || !input.value.trim()) return;
try {
const name = input.value.trim();
await window.electronAPI.profile.create(name);
input.value = '';
await loadProfiles();
} catch (error) {
console.error('Failed to create profile:', error);
alert('Failed to create profile: ' + error.message);
}
};
window.deleteProfile = async (id) => {
if (!confirm('Are you sure you want to delete this profile? parameters and mods configuration will be lost.')) return;
try {
await window.electronAPI.profile.delete(id);
await loadProfiles();
} catch (error) {
console.error('Failed to delete profile:', error);
alert('Failed to delete profile: ' + error.message);
}
};
window.switchProfile = async (id) => {
try {
if (window.LauncherUI) window.LauncherUI.showProgress();
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Switching Profile...' });
await window.electronAPI.profile.activate(id);
// Refresh UI
await loadProfiles();
// Refresh Mods
if (window.modsManager) {
if (window.modsManager.loadInstalledMods) await window.modsManager.loadInstalledMods();
if (window.modsManager.loadBrowseMods) await window.modsManager.loadBrowseMods();
}
// Close dropdown
const dropdown = document.getElementById('profileDropdown');
if (dropdown) dropdown.classList.remove('show');
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Profile Switched!' });
setTimeout(() => window.LauncherUI.hideProgress(), 1000);
}
} catch (error) {
console.error('Failed to switch profile:', error);
alert('Failed to switch profile: ' + error.message);
if (window.LauncherUI) window.LauncherUI.hideProgress();
}
};
export async function launch() {
if (isDownloading || (playBtn && playBtn.disabled)) return;
let playerName = 'Player';
if (window.SettingsAPI && window.SettingsAPI.getCurrentPlayerName) {
playerName = window.SettingsAPI.getCurrentPlayerName();
} else if (playerNameInput && playerNameInput.value.trim()) {
playerName = playerNameInput.value.trim();
}
let javaPath = '';
if (window.SettingsAPI && window.SettingsAPI.getCurrentJavaPath) {
javaPath = window.SettingsAPI.getCurrentJavaPath();
}
if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true;
if (playBtn) {
playBtn.disabled = true;
playText.textContent = 'LAUNCHING...';
}
try {
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Starting game...' });
if (window.electronAPI && window.electronAPI.launchGame) {
const result = await window.electronAPI.launchGame(playerName, javaPath, '');
isDownloading = false;
if (window.LauncherUI) {
window.LauncherUI.hideProgress();
}
resetPlayButton();
if (result.success) {
if (window.electronAPI.minimizeWindow) {
setTimeout(() => {
window.electronAPI.minimizeWindow();
}, 500);
}
} else {
console.error('Launch failed:', result.error);
}
} else {
isDownloading = false;
if (window.LauncherUI) {
window.LauncherUI.hideProgress();
}
resetPlayButton();
}
} catch (error) {
isDownloading = false;
if (window.LauncherUI) {
window.LauncherUI.hideProgress();
}
resetPlayButton();
console.error('Launch error:', error);
}
}
function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCancel = null, confirmText = 'Confirm', cancelText = 'Cancel') {
const existingModal = document.querySelector('.custom-confirm-modal');
if (existingModal) {
existingModal.remove();
}
const modal = document.createElement('div');
modal.className = 'custom-confirm-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.className = 'custom-confirm-dialog';
dialog.style.cssText = `
background: #1f2937;
border-radius: 12px;
padding: 0;
min-width: 400px;
max-width: 500px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(239, 68, 68, 0.3);
transform: scale(0.9);
transition: transform 0.3s ease;
`;
dialog.innerHTML = `
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">${title}</h3>
</div>
</div>
<div style="padding: 24px; color: #e5e7eb;">
<p style="margin: 0; line-height: 1.5; font-size: 1rem;">${message}</p>
</div>
<div style="padding: 20px 24px; display: flex; gap: 12px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
<button class="custom-confirm-cancel" style="
background: transparent;
color: #9ca3af;
border: 1px solid rgba(156, 163, 175, 0.3);
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">${cancelText}</button>
<button class="custom-confirm-action" style="
background: #ef4444;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">${confirmText}</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
// Animate in
setTimeout(() => {
modal.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
// Event handlers
const cancelBtn = dialog.querySelector('.custom-confirm-cancel');
const actionBtn = dialog.querySelector('.custom-confirm-action');
const closeModal = () => {
modal.style.opacity = '0';
dialog.style.transform = 'scale(0.9)';
setTimeout(() => {
modal.remove();
}, 300);
};
cancelBtn.onclick = () => {
closeModal();
if (onCancel) onCancel();
};
actionBtn.onclick = () => {
closeModal();
onConfirm();
};
modal.onclick = (e) => {
if (e.target === modal) {
closeModal();
if (onCancel) onCancel();
}
};
// Escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
closeModal();
if (onCancel) onCancel();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
export async function uninstallGame() {
showCustomConfirm(
'Are you sure you want to uninstall Hytale? All game files will be deleted.',
'Uninstall Game',
async () => {
await performUninstall();
},
null,
'Uninstall',
'Cancel'
);
}
async function performUninstall() {
if (window.LauncherUI) window.LauncherUI.showProgress();
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Uninstalling game...' });
if (uninstallBtn) uninstallBtn.disabled = true;
try {
if (window.electronAPI && window.electronAPI.uninstallGame) {
const result = await window.electronAPI.uninstallGame();
if (result.success) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(false);
}, 2000);
}
} else {
throw new Error(result.error || 'Uninstall failed');
}
} else {
setTimeout(() => {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: 'Game uninstalled successfully!' });
setTimeout(() => {
window.LauncherUI.hideProgress();
window.LauncherUI.showLauncherOrInstall(false);
}, 2000);
}
}, 2000);
}
} catch (error) {
if (window.LauncherUI) {
window.LauncherUI.updateProgress({ message: `Uninstall failed: ${error.message}` });
setTimeout(() => window.LauncherUI.hideProgress(), 3000);
}
} finally {
if (uninstallBtn) uninstallBtn.disabled = false;
}
}
function resetPlayButton() {
isDownloading = false;
if (playBtn) {
playBtn.disabled = false;
playText.textContent = 'PLAY';
}
}
async function savePlayerName() {
try {
if (window.electronAPI && window.electronAPI.saveSettings) {
const playerName = (playerNameInput ? playerNameInput.value.trim() : '') || 'Player';
await window.electronAPI.saveSettings({ playerName });
}
} catch (error) {
console.error('Error saving player name:', error);
}
}
async function saveJavaPath() {
try {
if (window.electronAPI && window.electronAPI.saveSettings) {
const javaPath = (javaPathInput ? javaPathInput.value.trim() : '') || '';
await window.electronAPI.saveSettings({ javaPath });
}
} catch (error) {
console.error('Error saving Java path:', error);
}
}
function toggleCustomJava() {
if (!customJavaOptions) return;
if (customJavaCheck && customJavaCheck.checked) {
customJavaOptions.style.display = 'block';
} else {
customJavaOptions.style.display = 'none';
if (customJavaPath) customJavaPath.value = '';
saveCustomJavaPath('');
}
}
async function browseJavaPath() {
try {
if (window.electronAPI && window.electronAPI.browseJavaPath) {
const result = await window.electronAPI.browseJavaPath();
if (result && result.filePaths && result.filePaths.length > 0) {
const selectedPath = result.filePaths[0];
if (customJavaPath) {
customJavaPath.value = selectedPath;
}
await saveCustomJavaPath(selectedPath);
}
}
} catch (error) {
console.error('Error browsing Java path:', error);
}
}
async function saveCustomJavaPath(path) {
try {
if (window.electronAPI && window.electronAPI.saveJavaPath) {
await window.electronAPI.saveJavaPath(path);
}
} catch (error) {
console.error('Error saving custom Java path:', error);
}
}
async function loadCustomJavaPath() {
try {
if (window.electronAPI && window.electronAPI.loadJavaPath) {
const savedPath = await window.electronAPI.loadJavaPath();
if (savedPath && savedPath.trim()) {
if (customJavaPath) {
customJavaPath.value = savedPath;
}
if (customJavaCheck) {
customJavaCheck.checked = true;
}
if (customJavaOptions) {
customJavaOptions.style.display = 'block';
}
}
}
} catch (error) {
console.error('Error loading custom Java path:', error);
}
}
window.launch = launch;
window.uninstallGame = uninstallGame;
document.addEventListener('DOMContentLoaded', setupLauncher);

View File

@@ -1,732 +0,0 @@
const API_KEY = '$2a$10$bqk254NMZOWVTzLVJCcxEOmhcyUujKxA5xk.kQCN9q0KNYFJd5b32';
const CURSEFORGE_API = 'https://api.curseforge.com/v1';
const HYTALE_GAME_ID = 70216;
let installedMods = [];
let browseMods = [];
let searchQuery = '';
let modsPage = 0;
let modsPageSize = 20;
let modsTotalPages = 1;
export async function initModsManager() {
setupModsEventListeners();
await loadInstalledMods();
await loadBrowseMods();
}
function setupModsEventListeners() {
const searchInput = document.getElementById('modsSearch');
if (searchInput) {
let searchTimeout;
searchInput.addEventListener('input', (e) => {
searchQuery = e.target.value.toLowerCase().trim();
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
modsPage = 0;
loadBrowseMods();
}, 500);
});
}
const myModsBtn = document.getElementById('myModsBtn');
if (myModsBtn) {
myModsBtn.addEventListener('click', openMyModsModal);
}
const closeModalBtn = document.getElementById('closeMyModsModal');
if (closeModalBtn) {
closeModalBtn.addEventListener('click', closeMyModsModal);
}
const modal = document.getElementById('myModsModal');
if (modal) {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeMyModsModal();
}
});
}
const prevPageBtn = document.getElementById('prevPage');
const nextPageBtn = document.getElementById('nextPage');
if (prevPageBtn) {
prevPageBtn.addEventListener('click', () => {
if (modsPage > 0) {
modsPage--;
loadBrowseMods();
}
});
}
if (nextPageBtn) {
nextPageBtn.addEventListener('click', () => {
if (modsPage < modsTotalPages - 1) {
modsPage++;
loadBrowseMods();
}
});
}
}
function openMyModsModal() {
const modal = document.getElementById('myModsModal');
if (modal) {
modal.classList.add('active');
loadInstalledMods();
}
}
function closeMyModsModal() {
const modal = document.getElementById('myModsModal');
if (modal) {
modal.classList.remove('active');
}
}
async function loadInstalledMods() {
try {
const modsPath = await window.electronAPI?.getModsPath();
if (!modsPath) {
showInstalledModsError('Could not get mods directory');
return;
}
const mods = await window.electronAPI?.loadInstalledMods(modsPath);
installedMods = mods || [];
displayInstalledMods(installedMods);
} catch (error) {
console.error('Error loading installed mods:', error);
showInstalledModsError('Failed to load installed mods');
}
}
function displayInstalledMods(mods) {
const modsContainer = document.getElementById('installedModsList');
if (!modsContainer) return;
if (mods.length === 0) {
modsContainer.innerHTML = `
<div class=\"empty-installed-mods\">
<i class=\"fas fa-box-open\"></i>
<h4>No Mods Installed</h4>
<p>Add mods from CurseForge or import local files</p>
</div>
`;
return;
}
modsContainer.innerHTML = mods.map(mod => createInstalledModCard(mod)).join('');
mods.forEach(mod => {
const toggleBtn = document.getElementById(`toggle-installed-${mod.id}`);
const deleteBtn = document.getElementById(`delete-installed-${mod.id}`);
if (toggleBtn) {
toggleBtn.addEventListener('click', () => toggleMod(mod.id));
}
if (deleteBtn) {
deleteBtn.addEventListener('click', () => deleteMod(mod.id));
}
});
}
function createInstalledModCard(mod) {
const statusClass = mod.enabled ? 'text-primary' : 'text-zinc-500';
const statusText = mod.enabled ? 'ACTIVE' : 'DISABLED';
const toggleBtnClass = mod.enabled ? 'btn-disable' : 'btn-enable';
const toggleBtnText = mod.enabled ? 'DISABLE' : 'ENABLE';
const toggleIcon = mod.enabled ? 'fa-pause' : 'fa-play';
return `
<div class="installed-mod-card" data-mod-id="${mod.id}">
<div class="installed-mod-icon">
<i class="fas fa-cube"></i>
</div>
<div class="installed-mod-info">
<div class="installed-mod-header">
<h4 class="installed-mod-name">${mod.name}</h4>
<span class="installed-mod-version">v${mod.version}</span>
</div>
<p class="installed-mod-description">${mod.description || 'No description available'}</p>
</div>
<div class="installed-mod-actions">
<div class="installed-mod-status ${statusClass}">
<i class="fas fa-circle"></i>
${statusText}
</div>
<div class="installed-mod-buttons">
<button id="delete-installed-${mod.id}" class="installed-mod-btn-icon" title="Delete mod">
<i class="fas fa-trash"></i>
</button>
<button id="toggle-installed-${mod.id}" class="installed-mod-btn-toggle ${toggleBtnClass}">
<i class="fas ${toggleIcon}"></i>
${toggleBtnText}
</button>
</div>
</div>
</div>
`;
}
async function loadBrowseMods() {
const browseContainer = document.getElementById('browseModsList');
if (!browseContainer) return;
browseContainer.innerHTML = '<div class=\"loading-mods\"><div class=\"loading-spinner\"></div><span>Loading mods from CurseForge...</span></div>';
try {
if (!API_KEY || API_KEY.length < 10) {
browseContainer.innerHTML = `
<div class=\"empty-browse-mods\">
<i class=\"fas fa-key\"></i>
<h4>API Key Required</h4>
<p>CurseForge API key is needed to browse mods</p>
</div>
`;
return;
}
const offset = modsPage * modsPageSize;
let url = `${CURSEFORGE_API}/mods/search?gameId=${HYTALE_GAME_ID}&pageSize=${modsPageSize}&sortOrder=desc&sortField=6&index=${offset}`;
if (searchQuery && searchQuery.length > 0) {
url += `&searchFilter=${encodeURIComponent(searchQuery)}`;
}
console.log('Fetching mods from page', modsPage + 1, 'offset:', offset, 'search:', searchQuery || 'none', 'URL:', url);
const response = await fetch(url, {
headers: {
'x-api-key': API_KEY,
'Accept': 'application/json'
}
});
console.log('Response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('API Error Response:', errorText);
throw new Error(`CurseForge API error: ${response.status} - ${errorText}`);
}
const data = await response.json();
console.log('API Response data:', data);
console.log('Total mods found:', data.data?.length || 0);
browseMods = (data.data || []).map(mod => ({
id: mod.id.toString(),
name: mod.name,
slug: mod.slug,
summary: mod.summary || 'No description available',
downloadCount: mod.downloadCount || 0,
author: mod.authors?.[0]?.name || 'Unknown',
version: mod.latestFiles?.[0]?.displayName || 'Unknown',
thumbnailUrl: mod.logo?.thumbnailUrl || null,
websiteUrl: mod.links?.websiteUrl || null,
modId: mod.id,
fileId: mod.latestFiles?.[0]?.id,
fileName: mod.latestFiles?.[0]?.fileName,
downloadUrl: mod.latestFiles?.[0]?.downloadUrl
}));
console.log('Processed mods:', browseMods.length);
modsTotalPages = Math.ceil((data.pagination?.totalCount || 1) / modsPageSize);
displayBrowseMods(browseMods);
updatePagination();
} catch (error) {
console.error('Error loading browse mods:', error);
browseContainer.innerHTML = `
<div class=\"empty-browse-mods error\">
<i class=\"fas fa-exclamation-triangle\"></i>
<h4>API Error</h4>
<p>Failed to load mods from CurseForge</p>
<small>${error.message}</small>
</div>
`;
}
}
function displayBrowseMods(mods) {
const browseContainer = document.getElementById('browseModsList');
if (!browseContainer) return;
if (mods.length === 0) {
browseContainer.innerHTML = `
<div class=\"empty-browse-mods\">
<i class=\"fas fa-search\"></i>
<h4>No Mods Found</h4>
<p>Try adjusting your search</p>
</div>
`;
return;
}
browseContainer.innerHTML = mods.map(mod => createBrowseModCard(mod)).join('');
mods.forEach(mod => {
const installBtn = document.getElementById(`install-${mod.id}`);
if (installBtn) {
installBtn.addEventListener('click', () => downloadAndInstallMod(mod));
}
});
}
function createBrowseModCard(mod) {
const isInstalled = installedMods.some(installed => {
// Check by CurseForge ID (most reliable)
if (installed.curseForgeId && installed.curseForgeId.toString() === mod.id.toString()) {
return true;
}
// Check by exact name match for manually installed mods
if (installed.name.toLowerCase() === mod.name.toLowerCase()) {
return true;
}
return false;
});
return `
<div class=\"mod-card ${isInstalled ? 'installed' : ''}\" data-mod-id=\"${mod.id}\">
<div class=\"mod-image\">
${mod.thumbnailUrl ?
`<img src=\"${mod.thumbnailUrl}\" alt=\"${mod.name}\" onerror=\"this.parentElement.innerHTML='<i class=\\\"fas fa-puzzle-piece\\\"></i>'\">` :
`<i class=\"fas fa-puzzle-piece\"></i>`
}
</div>
<div class=\"mod-info\">
<div class=\"mod-header\">
<h3 class=\"mod-name\">${mod.name}</h3>
<span class=\"mod-version\">${mod.version}</span>
</div>
<p class=\"mod-description\">${mod.summary}</p>
<div class=\"mod-meta\">
<span class=\"mod-meta-item\">
<i class=\"fas fa-user\"></i>
${mod.author}
</span>
<span class=\"mod-meta-item\">
<i class=\"fas fa-download\"></i>
${formatNumber(mod.downloadCount)}
</span>
</div>
</div>
<div class=\"mod-actions\">
<button id=\"view-${mod.id}\" class=\"mod-btn-toggle bg-blue-600 text-white hover:bg-blue-700\" onclick=\"window.modsManager.viewModPage(${mod.id})\">
<i class=\"fas fa-external-link-alt\"></i>
VIEW
</button>
${!isInstalled ?
`<button id=\"install-${mod.id}\" class=\"mod-btn-toggle bg-primary text-black hover:bg-primary/80\">
<i class=\"fas fa-download\"></i>
INSTALL
</button>` :
`<button class=\"mod-btn-toggle bg-white/10 text-white\" disabled>
<i class=\"fas fa-check\"></i>
INSTALLED
</button>`
}
</div>
</div>
`;
}
async function downloadAndInstallMod(modInfo) {
try {
window.LauncherUI?.showProgress(`Downloading ${modInfo.name}...`);
const result = await window.electronAPI?.downloadMod(modInfo);
if (result?.success) {
const newMod = {
id: result.modInfo.id,
name: modInfo.name,
version: modInfo.version,
description: modInfo.summary,
author: modInfo.author,
enabled: true,
fileName: result.fileName,
fileSize: result.modInfo.fileSize,
dateInstalled: new Date().toISOString(),
curseForgeId: modInfo.modId,
curseForgeFileId: modInfo.fileId
};
installedMods.push(newMod);
await loadInstalledMods();
await loadBrowseMods();
window.LauncherUI?.hideProgress();
showNotification(`${modInfo.name} installed successfully! 🎉`, 'success');
} else {
throw new Error(result?.error || 'Failed to download mod');
}
} catch (error) {
console.error('Error downloading mod:', error);
window.LauncherUI?.hideProgress();
showNotification('Failed to download mod: ' + error.message, 'error');
}
}
async function toggleMod(modId) {
try {
window.LauncherUI?.showProgress('Toggling mod...');
const modsPath = await window.electronAPI?.getModsPath();
const result = await window.electronAPI?.toggleMod(modId, modsPath);
if (result?.success) {
await loadInstalledMods();
window.LauncherUI?.hideProgress();
} else {
throw new Error(result?.error || 'Failed to toggle mod');
}
} catch (error) {
console.error('Error toggling mod:', error);
window.LauncherUI?.hideProgress();
showNotification('Failed to toggle mod: ' + error.message, 'error');
}
}
async function deleteMod(modId) {
const mod = installedMods.find(m => m.id === modId);
if (!mod) return;
showConfirmModal(
`Are you sure you want to delete "${mod.name}"? This action cannot be undone.`,
async () => {
try {
window.LauncherUI?.showProgress('Deleting mod...');
const modsPath = await window.electronAPI?.getModsPath();
const result = await window.electronAPI?.uninstallMod(modId, modsPath);
if (result?.success) {
await loadInstalledMods();
await loadBrowseMods();
window.LauncherUI?.hideProgress();
showNotification(`"${mod.name}" deleted successfully`, 'success');
} else {
throw new Error(result?.error || 'Failed to delete mod');
}
} catch (error) {
console.error('Error deleting mod:', error);
window.LauncherUI?.hideProgress();
showNotification('Failed to delete mod: ' + error.message, 'error');
}
}
);
}
function formatNumber(num) {
if (!num) return '0';
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
}
function showNotification(message, type = 'info', duration = 4000) {
const existing = document.querySelector(`.mod-notification.${type}`);
if (existing) {
existing.remove();
}
const notification = document.createElement('div');
notification.className = `mod-notification ${type}`;
const icons = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
info: 'fa-info-circle',
warning: 'fa-exclamation-triangle'
};
const colors = {
success: '#10b981',
error: '#ef4444',
info: '#3b82f6',
warning: '#f59e0b'
};
notification.innerHTML = `
<div class="notification-content">
<i class="fas ${icons[type]}"></i>
<span>${message}</span>
</div>
<button class="notification-close" onclick="this.parentElement.remove()">
<i class="fas fa-times"></i>
</button>
`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${colors[type]};
color: white;
padding: 16px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 10000;
min-width: 300px;
max-width: 400px;
transform: translateX(100%);
transition: transform 0.3s ease;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
font-weight: 500;
`;
const contentStyle = `
display: flex;
align-items: center;
gap: 10px;
flex: 1;
`;
const closeStyle = `
background: none;
border: none;
color: white;
cursor: pointer;
padding: 4px;
border-radius: 4px;
opacity: 0.8;
transition: opacity 0.2s;
margin-left: 10px;
`;
notification.querySelector('.notification-content').style.cssText = contentStyle;
notification.querySelector('.notification-close').style.cssText = closeStyle;
document.body.appendChild(notification);
// Animate in
setTimeout(() => {
notification.style.transform = 'translateX(0)';
}, 10);
// Auto remove
setTimeout(() => {
if (notification.parentElement) {
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
notification.remove();
}, 300);
}
}, duration);
}
function showConfirmModal(message, onConfirm, onCancel = null) {
const existingModal = document.querySelector('.mod-confirm-modal');
if (existingModal) {
existingModal.remove();
}
const modal = document.createElement('div');
modal.className = 'mod-confirm-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.className = 'mod-confirm-dialog';
dialog.style.cssText = `
background: #1f2937;
border-radius: 12px;
padding: 0;
min-width: 400px;
max-width: 500px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(239, 68, 68, 0.3);
transform: scale(0.9);
transition: transform 0.3s ease;
`;
dialog.innerHTML = `
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">Confirm Deletion</h3>
</div>
</div>
<div style="padding: 24px; color: #e5e7eb;">
<p style="margin: 0; line-height: 1.5; font-size: 1rem;">${message}</p>
</div>
<div style="padding: 20px 24px; display: flex; gap: 12px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
<button class="mod-confirm-cancel" style="
background: transparent;
color: #9ca3af;
border: 1px solid rgba(156, 163, 175, 0.3);
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">Cancel</button>
<button class="mod-confirm-delete" style="
background: #ef4444;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">Delete</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
// Animate in
setTimeout(() => {
modal.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
// Event handlers
const cancelBtn = dialog.querySelector('.mod-confirm-cancel');
const deleteBtn = dialog.querySelector('.mod-confirm-delete');
const closeModal = () => {
modal.style.opacity = '0';
dialog.style.transform = 'scale(0.9)';
setTimeout(() => {
modal.remove();
}, 300);
};
cancelBtn.onclick = () => {
closeModal();
if (onCancel) onCancel();
};
deleteBtn.onclick = () => {
closeModal();
onConfirm();
};
modal.onclick = (e) => {
if (e.target === modal) {
closeModal();
if (onCancel) onCancel();
}
};
// Escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
closeModal();
if (onCancel) onCancel();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
function updatePagination() {
const currentPageEl = document.getElementById('currentPage');
const totalPagesEl = document.getElementById('totalPages');
const prevBtn = document.getElementById('prevPage');
const nextBtn = document.getElementById('nextPage');
if (currentPageEl) currentPageEl.textContent = modsPage + 1;
if (totalPagesEl) totalPagesEl.textContent = modsTotalPages;
if (prevBtn) {
prevBtn.disabled = modsPage === 0;
prevBtn.style.opacity = modsPage === 0 ? '0.5' : '1';
prevBtn.style.cursor = modsPage === 0 ? 'not-allowed' : 'pointer';
}
if (nextBtn) {
nextBtn.disabled = modsPage >= modsTotalPages - 1;
nextBtn.style.opacity = modsPage >= modsTotalPages - 1 ? '0.5' : '1';
nextBtn.style.cursor = modsPage >= modsTotalPages - 1 ? 'not-allowed' : 'pointer';
}
}
function showInstalledModsError(message) {
const modsContainer = document.getElementById('installedModsList');
if (!modsContainer) return;
modsContainer.innerHTML = `
<div class=\"empty-installed-mods error\">
<i class=\"fas fa-exclamation-triangle\"></i>
<h4>Error</h4>
<p>${message}</p>
</div>
`;
}
function viewModPage(modId) {
console.log('Looking for mod with ID:', modId, 'Type:', typeof modId);
console.log('Available mods:', browseMods.map(m => ({ id: m.id, name: m.name, type: typeof m.id })));
const mod = browseMods.find(m => m.id.toString() === modId.toString());
if (mod) {
console.log('Found mod:', mod.name);
let modUrl;
if (mod.websiteUrl && mod.websiteUrl.includes('curseforge.com')) {
modUrl = mod.websiteUrl;
} else if (mod.slug) {
modUrl = `https://www.curseforge.com/hytale/mods/${mod.slug}`;
} else {
const nameSlug = mod.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
modUrl = `https://www.curseforge.com/hytale/mods/${nameSlug}`;
}
console.log('Opening URL:', modUrl);
if (window.electronAPI && window.electronAPI.openExternalLink) {
window.electronAPI.openExternalLink(modUrl);
} else {
if (window.electronAPI && window.electronAPI.shell) {
window.electronAPI.shell.openExternal(modUrl);
} else {
window.open(modUrl, '_blank');
}
}
} else {
console.error('Mod not found with ID:', modId);
showNotification('Mod information not found', 'error');
}
}
window.modsManager = {
toggleMod,
deleteMod,
openMyModsModal,
closeMyModsModal,
viewModPage,
loadInstalledMods,
loadBrowseMods
};
document.addEventListener('DOMContentLoaded', initModsManager);

View File

@@ -1,124 +0,0 @@
let newsData = [];
export async function loadNews() {
try {
if (window.electronAPI && window.electronAPI.getHytaleNews) {
try {
const realNews = await window.electronAPI.getHytaleNews();
if (realNews && realNews.length > 0) {
newsData = realNews.slice(0, 10).map((article, index) => ({
id: index + 1,
title: article.title,
summary: article.description,
type: "NEWS",
image: article.imageUrl || '',
date: formatDate(article.date),
url: article.destUrl
}));
displayHomeNews(newsData.slice(0, 5));
displayFullNews(newsData);
} else {
showErrorNews();
}
} catch (error) {
console.log('Failed to load news:', error.message);
showErrorNews();
}
} else {
showErrorNews();
}
} catch (error) {
console.error('Error loading news:', error);
showErrorNews();
}
}
function displayHomeNews(news) {
const newsGrid = document.getElementById('newsGrid');
if (!newsGrid) return;
newsGrid.innerHTML = news.map(article => `
<div class="news-item news-card" onclick="openNewsDetails(${article.id})">
<div class="news-image" style="background-image: url('${article.image}');"></div>
<div class="news-overlay">
<span class="news-type">${article.type}</span>
<span class="news-date">${article.date}</span>
</div>
<div class="news-content">
<h3 class="news-title">${article.title}</h3>
<p class="news-summary">${article.summary}</p>
</div>
</div>
`).join('');
}
function displayFullNews(news) {
const allNewsGrid = document.getElementById('allNewsGrid');
if (!allNewsGrid) return;
allNewsGrid.innerHTML = news.map(article => `
<div class="news-item news-card" onclick="openNewsDetails(${article.id})">
<div class="news-image" style="background-image: url('${article.image}');"></div>
<div class="news-overlay">
<span class="news-type">${article.type}</span>
<span class="news-date">${article.date}</span>
</div>
<div class="news-content">
<h3 class="news-title">${article.title}</h3>
<p class="news-summary">${article.summary}</p>
</div>
</div>
`).join('');
}
function showErrorNews() {
const newsGrid = document.getElementById('newsGrid');
if (newsGrid) {
newsGrid.innerHTML = `
<div class="loading-news">
<i class="fas fa-exclamation-triangle text-4xl mb-4 text-yellow-500"></i>
<span>Unable to load news</span>
</div>
`;
}
}
function openNewsDetails(newsId) {
const article = newsData.find(item => item.id === newsId);
if (article && article.url) {
openNewsArticle(article.url);
} else {
console.log('Opening news article:', article);
}
}
function openNewsArticle(url) {
if (url && url !== '#' && window.electronAPI && window.electronAPI.openExternal) {
window.electronAPI.openExternal(url);
}
}
function formatDate(dateString) {
if (!dateString) return 'RECENTLY';
const date = new Date(dateString);
const now = new Date();
const diffTime = Math.abs(now - date);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 1) return '1 DAY AGO';
if (diffDays < 7) return `${diffDays} DAYS AGO`;
if (diffDays < 30) return `${Math.ceil(diffDays / 7)} WEEKS AGO`;
return date.toLocaleDateString();
}
window.openNewsDetails = openNewsDetails;
window.navigateToPage = (page) => {
if (window.LauncherUI) {
window.LauncherUI.showPage(`${page}-page`);
window.LauncherUI.setActiveNav(page);
}
};
document.addEventListener('DOMContentLoaded', loadNews);

View File

@@ -1,154 +0,0 @@
const API_URL = 'https://api.hytalef2p.com/api';
let updateInterval = null;
let currentUserId = null;
export async function initPlayersCounter() {
setupPlayersCounter();
if (window.electronAPI && window.electronAPI.getUserId) {
currentUserId = await window.electronAPI.getUserId();
} else {
console.error('Electron API not available');
return;
}
let username = 'Player';
if (window.electronAPI.loadUsername) {
const savedUsername = await window.electronAPI.loadUsername();
if (savedUsername) username = savedUsername;
}
await registerPlayer(username, currentUserId);
await fetchPlayerStats();
startAutoUpdate();
}
function setupPlayersCounter() {
const counterElement = document.getElementById('playersOnlineCounter');
if (!counterElement) {
console.warn('Players counter element not found');
}
}
async function fetchPlayerStats() {
try {
const response = await fetch(`${API_URL}/players/stats`);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
updateCounterDisplay(data);
} catch (error) {
console.error('Error fetching player stats:', error);
updateCounterDisplay({ online: 0, peak: 0 });
}
}
function updateCounterDisplay(stats) {
const counterElement = document.getElementById('playersOnlineCounter');
const onlineCount = document.getElementById('onlineCount');
if (onlineCount) {
onlineCount.textContent = stats.online || 0;
}
if (counterElement) {
counterElement.classList.add('updated');
setTimeout(() => {
counterElement.classList.remove('updated');
}, 300);
}
}
async function registerPlayer(username, userId) {
try {
const response = await fetch(`${API_URL}/players/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, userId })
});
if (!response.ok) {
throw new Error(`Failed to register player: ${response.status}`);
}
const data = await response.json();
currentUserId = userId;
console.log('Player registered:', data);
await fetchPlayerStats();
return data;
} catch (error) {
console.error('Error registering player:', error);
return null;
}
}
async function unregisterPlayer(userId) {
try {
const response = await fetch(`${API_URL}/players/unregister`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ userId })
});
if (!response.ok) {
throw new Error(`Failed to unregister player: ${response.status}`);
}
const data = await response.json();
currentUserId = null;
console.log('Player unregistered:', data);
await fetchPlayerStats();
return data;
} catch (error) {
console.error('Error unregistering player:', error);
return null;
}
}
function startAutoUpdate() {
updateInterval = setInterval(async () => {
await fetchPlayerStats();
if (currentUserId) {
const username = window.LauncherState?.username || 'Player';
await registerPlayer(username, currentUserId);
}
}, 3000);
}
function stopAutoUpdate() {
if (updateInterval) {
clearInterval(updateInterval);
updateInterval = null;
}
}
window.addEventListener('beforeunload', () => {
if (currentUserId) {
const data = JSON.stringify({ userId: currentUserId });
navigator.sendBeacon(`${API_URL}/players/unregister`, data);
}
stopAutoUpdate();
});
window.PlayersAPI = {
register: registerPlayer,
unregister: unregisterPlayer,
fetchStats: fetchPlayerStats
};
document.addEventListener('DOMContentLoaded', initPlayersCounter);

View File

@@ -1,38 +0,0 @@
import './ui.js';
import './install.js';
import './launcher.js';
import './news.js';
import './mods.js';
import './players.js';
import './chat.js';
import './settings.js';
window.closeDiscordNotification = function() {
const notification = document.getElementById('discordNotification');
if (notification) {
notification.classList.add('hidden');
setTimeout(() => {
notification.style.display = 'none';
}, 300);
}
};
document.addEventListener('DOMContentLoaded', () => {
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';
}
}
});
const originalClose = window.closeDiscordNotification;
window.closeDiscordNotification = function() {
localStorage.setItem('discordNotificationDismissed', 'true');
originalClose();
};

View File

@@ -1,728 +0,0 @@
let customJavaCheck;
let customJavaOptions;
let customJavaPath;
let browseJavaBtn;
let settingsPlayerName;
let discordRPCCheck;
// UUID Management elements
let currentUuidDisplay;
let copyUuidBtn;
let regenerateUuidBtn;
let manageUuidsBtn;
let uuidModal;
let uuidModalClose;
let modalCurrentUuid;
let modalCopyUuidBtn;
let modalRegenerateUuidBtn;
let generateNewUuidBtn;
let uuidList;
let customUuidInput;
let setCustomUuidBtn;
function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCancel = null, confirmText = 'Confirm', cancelText = 'Cancel') {
const existingModal = document.querySelector('.custom-confirm-modal');
if (existingModal) {
existingModal.remove();
}
const modal = document.createElement('div');
modal.className = 'custom-confirm-modal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
`;
const dialog = document.createElement('div');
dialog.className = 'custom-confirm-dialog';
dialog.style.cssText = `
background: #1f2937;
border-radius: 12px;
padding: 0;
min-width: 400px;
max-width: 500px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(147, 51, 234, 0.3);
transform: scale(0.9);
transition: transform 0.3s ease;
`;
dialog.innerHTML = `
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<div style="display: flex; align-items: center; gap: 12px; color: #9333ea;">
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">${title}</h3>
</div>
</div>
<div style="padding: 24px; color: #e5e7eb;">
<p style="margin: 0; line-height: 1.5; font-size: 1rem;">${message}</p>
</div>
<div style="padding: 20px 24px; display: flex; gap: 12px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
<button class="custom-confirm-cancel" style="
background: transparent;
color: #9ca3af;
border: 1px solid rgba(156, 163, 175, 0.3);
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">${cancelText}</button>
<button class="custom-confirm-action" style="
background: linear-gradient(135deg, #9333ea, #3b82f6);
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
">${confirmText}</button>
</div>
`;
modal.appendChild(dialog);
document.body.appendChild(modal);
// Animate in
setTimeout(() => {
modal.style.opacity = '1';
dialog.style.transform = 'scale(1)';
}, 10);
// Event handlers
const cancelBtn = dialog.querySelector('.custom-confirm-cancel');
const actionBtn = dialog.querySelector('.custom-confirm-action');
const closeModal = () => {
modal.style.opacity = '0';
dialog.style.transform = 'scale(0.9)';
setTimeout(() => {
modal.remove();
}, 300);
};
cancelBtn.onclick = () => {
closeModal();
if (onCancel) onCancel();
};
actionBtn.onclick = () => {
closeModal();
onConfirm();
};
modal.onclick = (e) => {
if (e.target === modal) {
closeModal();
if (onCancel) onCancel();
}
};
// Escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
closeModal();
if (onCancel) onCancel();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
export function initSettings() {
setupSettingsElements();
loadAllSettings();
}
function setupSettingsElements() {
customJavaCheck = document.getElementById('customJavaCheck');
customJavaOptions = document.getElementById('customJavaOptions');
customJavaPath = document.getElementById('customJavaPath');
browseJavaBtn = document.getElementById('browseJavaBtn');
settingsPlayerName = document.getElementById('settingsPlayerName');
discordRPCCheck = document.getElementById('discordRPCCheck');
// UUID Management elements
currentUuidDisplay = document.getElementById('currentUuid');
copyUuidBtn = document.getElementById('copyUuidBtn');
regenerateUuidBtn = document.getElementById('regenerateUuidBtn');
manageUuidsBtn = document.getElementById('manageUuidsBtn');
uuidModal = document.getElementById('uuidModal');
uuidModalClose = document.getElementById('uuidModalClose');
modalCurrentUuid = document.getElementById('modalCurrentUuid');
modalCopyUuidBtn = document.getElementById('modalCopyUuidBtn');
modalRegenerateUuidBtn = document.getElementById('modalRegenerateUuidBtn');
generateNewUuidBtn = document.getElementById('generateNewUuidBtn');
uuidList = document.getElementById('uuidList');
customUuidInput = document.getElementById('customUuidInput');
setCustomUuidBtn = document.getElementById('setCustomUuidBtn');
if (customJavaCheck) {
customJavaCheck.addEventListener('change', toggleCustomJava);
}
if (browseJavaBtn) {
browseJavaBtn.addEventListener('click', browseJavaPath);
}
if (settingsPlayerName) {
settingsPlayerName.addEventListener('change', savePlayerName);
}
if (discordRPCCheck) {
discordRPCCheck.addEventListener('change', saveDiscordRPC);
}
// UUID event listeners
if (copyUuidBtn) {
copyUuidBtn.addEventListener('click', copyCurrentUuid);
}
if (regenerateUuidBtn) {
regenerateUuidBtn.addEventListener('click', regenerateCurrentUuid);
}
if (manageUuidsBtn) {
manageUuidsBtn.addEventListener('click', openUuidModal);
}
if (uuidModalClose) {
uuidModalClose.addEventListener('click', closeUuidModal);
}
if (modalCopyUuidBtn) {
modalCopyUuidBtn.addEventListener('click', copyCurrentUuid);
}
if (modalRegenerateUuidBtn) {
modalRegenerateUuidBtn.addEventListener('click', regenerateCurrentUuid);
}
if (generateNewUuidBtn) {
generateNewUuidBtn.addEventListener('click', generateNewUuid);
}
if (setCustomUuidBtn) {
setCustomUuidBtn.addEventListener('click', setCustomUuid);
}
if (uuidModal) {
uuidModal.addEventListener('click', (e) => {
if (e.target === uuidModal) {
closeUuidModal();
}
});
}
}
function toggleCustomJava() {
if (!customJavaOptions) return;
if (customJavaCheck && customJavaCheck.checked) {
customJavaOptions.style.display = 'block';
} else {
customJavaOptions.style.display = 'none';
if (customJavaPath) customJavaPath.value = '';
saveCustomJavaPath('');
}
}
async function browseJavaPath() {
try {
if (window.electronAPI && window.electronAPI.browseJavaPath) {
const result = await window.electronAPI.browseJavaPath();
if (result && result.filePaths && result.filePaths.length > 0) {
const selectedPath = result.filePaths[0];
if (customJavaPath) {
customJavaPath.value = selectedPath;
}
await saveCustomJavaPath(selectedPath);
}
}
} catch (error) {
console.error('Error browsing Java path:', error);
}
}
async function saveCustomJavaPath(path) {
try {
if (window.electronAPI && window.electronAPI.saveJavaPath) {
await window.electronAPI.saveJavaPath(path);
}
} catch (error) {
console.error('Error saving custom Java path:', error);
}
}
async function loadCustomJavaPath() {
try {
if (window.electronAPI && window.electronAPI.loadJavaPath) {
const savedPath = await window.electronAPI.loadJavaPath();
if (savedPath && savedPath.trim()) {
if (customJavaPath) {
customJavaPath.value = savedPath;
}
if (customJavaCheck) {
customJavaCheck.checked = true;
}
if (customJavaOptions) {
customJavaOptions.style.display = 'block';
}
}
}
} catch (error) {
console.error('Error loading custom Java path:', error);
}
}
async function saveDiscordRPC() {
try {
if (window.electronAPI && window.electronAPI.saveDiscordRPC && discordRPCCheck) {
const enabled = discordRPCCheck.checked;
console.log('Saving Discord RPC setting:', enabled);
const result = await window.electronAPI.saveDiscordRPC(enabled);
if (result && result.success) {
console.log('Discord RPC setting saved successfully:', enabled);
// Feedback visuel pour l'utilisateur
if (enabled) {
showNotification('Discord Rich Presence enabled', 'success');
} else {
showNotification('Discord Rich Presence disabled', 'success');
}
} else {
throw new Error('Failed to save Discord RPC setting');
}
}
} catch (error) {
console.error('Error saving Discord RPC setting:', error);
showNotification('Failed to save Discord setting', 'error');
}
}
async function loadDiscordRPC() {
try {
if (window.electronAPI && window.electronAPI.loadDiscordRPC) {
const enabled = await window.electronAPI.loadDiscordRPC();
if (discordRPCCheck) {
discordRPCCheck.checked = enabled;
}
}
} catch (error) {
console.error('Error loading Discord RPC setting:', error);
}
}
async function savePlayerName() {
try {
if (!window.electronAPI || !settingsPlayerName) return;
const playerName = settingsPlayerName.value.trim();
if (!playerName) {
showNotification('Please enter a valid player name', 'error');
return;
}
await window.electronAPI.saveUsername(playerName);
showNotification('Player name saved successfully', 'success');
} catch (error) {
console.error('Error saving player name:', error);
showNotification('Failed to save player name', 'error');
}
}
async function loadPlayerName() {
try {
if (!window.electronAPI || !settingsPlayerName) return;
const savedName = await window.electronAPI.loadUsername();
if (savedName) {
settingsPlayerName.value = savedName;
}
} catch (error) {
console.error('Error loading player name:', error);
}
}
async function loadAllSettings() {
await loadCustomJavaPath();
await loadPlayerName();
await loadCurrentUuid();
await loadDiscordRPC();
}
async function openGameLocation() {
try {
if (window.electronAPI && window.electronAPI.openGameLocation) {
await window.electronAPI.openGameLocation();
}
} catch (error) {
console.error('Error opening game location:', error);
}
}
export function getCurrentJavaPath() {
if (customJavaCheck && customJavaCheck.checked && customJavaPath) {
return customJavaPath.value.trim();
}
return '';
}
export function getCurrentPlayerName() {
if (settingsPlayerName && settingsPlayerName.value.trim()) {
return settingsPlayerName.value.trim();
}
return 'Player';
}
window.openGameLocation = openGameLocation;
document.addEventListener('DOMContentLoaded', initSettings);
window.SettingsAPI = {
getCurrentJavaPath,
getCurrentPlayerName
};
async function loadCurrentUuid() {
try {
if (window.electronAPI && window.electronAPI.getCurrentUuid) {
const uuid = await window.electronAPI.getCurrentUuid();
if (uuid) {
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
}
}
} catch (error) {
console.error('Error loading current UUID:', error);
}
}
async function copyCurrentUuid() {
try {
const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value;
if (uuid && navigator.clipboard) {
await navigator.clipboard.writeText(uuid);
showNotification('UUID copied to clipboard!', 'success');
}
} catch (error) {
console.error('Error copying UUID:', error);
showNotification('Failed to copy UUID', 'error');
}
}
async function regenerateCurrentUuid() {
try {
if (window.electronAPI && window.electronAPI.resetCurrentUserUuid) {
showCustomConfirm(
'Are you sure you want to generate a new UUID? This will change your player identity.',
'Generate New UUID',
async () => {
await performRegenerateUuid();
},
null,
'Generate',
'Cancel'
);
} else {
console.error('electronAPI.resetCurrentUserUuid not available');
showNotification('UUID regeneration not available', 'error');
}
} catch (error) {
console.error('Error in regenerateCurrentUuid:', error);
showNotification('Failed to regenerate UUID', 'error');
}
}
async function performRegenerateUuid() {
try {
const result = await window.electronAPI.resetCurrentUserUuid();
if (result.success && result.uuid) {
if (currentUuidDisplay) currentUuidDisplay.value = result.uuid;
if (modalCurrentUuid) modalCurrentUuid.value = result.uuid;
showNotification('New UUID generated successfully!', 'success');
if (uuidModal && uuidModal.style.display !== 'none') {
await loadAllUuids();
}
} else {
throw new Error(result.error || 'Failed to generate new UUID');
}
} catch (error) {
console.error('Error regenerating UUID:', error);
showNotification(`Failed to regenerate UUID: ${error.message}`, 'error');
}
}
async function openUuidModal() {
try {
if (uuidModal) {
uuidModal.style.display = 'flex';
uuidModal.classList.add('active');
await loadAllUuids();
}
} catch (error) {
console.error('Error opening UUID modal:', error);
}
}
function closeUuidModal() {
if (uuidModal) {
uuidModal.classList.remove('active');
setTimeout(() => {
uuidModal.style.display = 'none';
}, 300);
}
}
async function loadAllUuids() {
try {
if (!uuidList) return;
uuidList.innerHTML = `
<div class="uuid-loading">
<i class="fas fa-spinner fa-spin"></i>
Loading UUIDs...
</div>
`;
if (window.electronAPI && window.electronAPI.getAllUuidMappings) {
const mappings = await window.electronAPI.getAllUuidMappings();
if (mappings.length === 0) {
uuidList.innerHTML = `
<div class="uuid-loading">
<i class="fas fa-info-circle"></i>
No UUIDs found
</div>
`;
return;
}
uuidList.innerHTML = '';
for (const mapping of mappings) {
const item = document.createElement('div');
item.className = `uuid-list-item${mapping.isCurrent ? ' current' : ''}`;
item.innerHTML = `
<div class="uuid-item-info">
<div class="uuid-item-username">${escapeHtml(mapping.username)}</div>
<div class="uuid-item-uuid">${mapping.uuid}</div>
</div>
<div class="uuid-item-actions">
${mapping.isCurrent ? '<div class="uuid-item-current-badge">Current</div>' : ''}
<button class="uuid-item-btn copy" onclick="copyUuid('${mapping.uuid}')" title="Copy UUID">
<i class="fas fa-copy"></i>
</button>
${!mapping.isCurrent ? `<button class="uuid-item-btn delete" onclick="deleteUuid('${escapeHtml(mapping.username)}')" title="Delete UUID">
<i class="fas fa-trash"></i>
</button>` : ''}
</div>
`;
uuidList.appendChild(item);
}
}
} catch (error) {
console.error('Error loading UUIDs:', error);
if (uuidList) {
uuidList.innerHTML = `
<div class="uuid-loading">
<i class="fas fa-exclamation-triangle"></i>
Error loading UUIDs
</div>
`;
}
}
}
async function generateNewUuid() {
try {
if (window.electronAPI && window.electronAPI.generateNewUuid) {
const newUuid = await window.electronAPI.generateNewUuid();
if (newUuid) {
if (customUuidInput) customUuidInput.value = newUuid;
showNotification('New UUID generated!', 'success');
}
}
} catch (error) {
console.error('Error generating new UUID:', error);
showNotification('Failed to generate new UUID', 'error');
}
}
async function setCustomUuid() {
try {
if (!customUuidInput || !customUuidInput.value.trim()) {
showNotification('Please enter a UUID', 'error');
return;
}
const uuid = customUuidInput.value.trim();
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(uuid)) {
showNotification('Invalid UUID format', 'error');
return;
}
showCustomConfirm(
'Are you sure you want to set this custom UUID? This will change your player identity.',
'Set Custom UUID',
async () => {
await performSetCustomUuid(uuid);
},
null,
'Set UUID',
'Cancel'
);
} catch (error) {
console.error('Error in setCustomUuid:', error);
showNotification('Failed to set custom UUID', 'error');
}
}
async function performSetCustomUuid(uuid) {
try {
if (window.electronAPI && window.electronAPI.setUuidForUser) {
const username = getCurrentPlayerName();
const result = await window.electronAPI.setUuidForUser(username, uuid);
if (result.success) {
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
if (modalCurrentUuid) modalCurrentUuid.value = uuid;
if (customUuidInput) customUuidInput.value = '';
showNotification('Custom UUID set successfully!', 'success');
await loadAllUuids();
} else {
throw new Error(result.error || 'Failed to set custom UUID');
}
}
} catch (error) {
console.error('Error setting custom UUID:', error);
showNotification(`Failed to set custom UUID: ${error.message}`, 'error');
}
}
window.copyUuid = async function(uuid) {
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(uuid);
showNotification('UUID copied to clipboard!', 'success');
}
} catch (error) {
console.error('Error copying UUID:', error);
showNotification('Failed to copy UUID', 'error');
}
};
window.deleteUuid = async function(username) {
try {
showCustomConfirm(
`Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`,
'Delete UUID',
async () => {
await performDeleteUuid(username);
},
null,
'Delete',
'Cancel'
);
} catch (error) {
console.error('Error in deleteUuid:', error);
showNotification('Failed to delete UUID', 'error');
}
};
async function performDeleteUuid(username) {
try {
if (window.electronAPI && window.electronAPI.deleteUuidForUser) {
const result = await window.electronAPI.deleteUuidForUser(username);
if (result.success) {
showNotification('UUID deleted successfully!', 'success');
await loadAllUuids();
} else {
throw new Error(result.error || 'Failed to delete UUID');
}
}
} catch (error) {
console.error('Error deleting UUID:', error);
showNotification(`Failed to delete UUID: ${error.message}`, 'error');
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 8px;
color: white;
font-weight: 600;
z-index: 10000;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
`;
if (type === 'success') {
notification.style.background = 'linear-gradient(135deg, #22c55e, #16a34a)';
} else if (type === 'error') {
notification.style.background = 'linear-gradient(135deg, #ef4444, #dc2626)';
} else {
notification.style.background = 'linear-gradient(135deg, #3b82f6, #2563eb)';
}
notification.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check' : type === 'error' ? 'exclamation-triangle' : 'info-circle'}"></i>
${message}
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '1';
notification.style.transform = 'translateX(0)';
}, 100);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}, 3000);
}

View File

@@ -1,508 +0,0 @@
let progressOverlay;
let progressBar;
let progressBarFill;
let progressText;
let progressPercent;
let progressSpeed;
let progressSize;
function showPage(pageId) {
const pages = document.querySelectorAll('.page');
pages.forEach(page => {
if (page.id === pageId) {
page.classList.add('active');
page.style.display = '';
} else {
page.classList.remove('active');
page.style.display = 'none';
}
});
}
function setActiveNav(page) {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
if (item.getAttribute('data-page') === page) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
function handleNavigation() {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', () => {
const page = item.getAttribute('data-page');
showPage(`${page}-page`);
setActiveNav(page);
});
});
}
function setupWindowControls() {
const minimizeBtn = document.querySelector('.window-controls .minimize');
const closeBtn = document.querySelector('.window-controls .close');
const windowControls = document.querySelector('.window-controls');
const header = document.querySelector('.header');
const profileSelector = document.querySelector('.profile-selector');
if (profileSelector) {
profileSelector.style.pointerEvents = 'auto';
profileSelector.style.zIndex = '10000';
}
if (windowControls) {
windowControls.style.pointerEvents = 'auto';
windowControls.style.zIndex = '10000';
}
if (header) {
header.style.webkitAppRegion = 'drag';
if (windowControls) {
windowControls.style.webkitAppRegion = 'no-drag';
}
if (profileSelector) {
profileSelector.style.webkitAppRegion = 'no-drag';
}
}
if (window.electronAPI) {
if (minimizeBtn) {
minimizeBtn.onclick = (e) => {
e.stopPropagation();
window.electronAPI.minimizeWindow();
};
}
if (closeBtn) {
closeBtn.onclick = (e) => {
e.stopPropagation();
window.electronAPI.closeWindow();
};
}
}
}
function showLauncherOrInstall(isInstalled) {
const launcher = document.getElementById('launcher-container');
const install = document.getElementById('install-page');
const sidebar = document.querySelector('.sidebar');
const gameTitle = document.querySelector('.game-title-section');
if (isInstalled) {
if (launcher) launcher.style.display = '';
if (install) install.style.display = 'none';
if (sidebar) sidebar.style.pointerEvents = 'auto';
if (gameTitle) gameTitle.style.display = '';
showPage('play-page');
setActiveNav('play');
} else {
if (launcher) launcher.style.display = 'none';
if (install) {
install.style.display = '';
install.classList.add('active');
}
if (sidebar) sidebar.style.pointerEvents = 'none';
if (gameTitle) gameTitle.style.display = 'none';
const pages = document.querySelectorAll('#launcher-container .page');
pages.forEach(page => page.classList.remove('active'));
}
}
function setupSidebarLogo() {
const logo = document.querySelector('.sidebar-logo img');
if (logo) {
logo.addEventListener('click', () => {
showPage('play-page');
setActiveNav('play');
});
}
}
function showProgress() {
if (progressOverlay) {
progressOverlay.style.display = 'block';
setTimeout(() => {
progressOverlay.style.opacity = '1';
progressOverlay.style.transform = 'translateY(0)';
}, 10);
}
}
function hideProgress() {
if (progressOverlay) {
progressOverlay.style.opacity = '0';
progressOverlay.style.transform = 'translateY(20px)';
setTimeout(() => {
progressOverlay.style.display = 'none';
}, 300);
}
}
function updateProgress(data) {
if (data.message && progressText) {
progressText.textContent = data.message;
}
if (data.percent !== null && data.percent !== undefined) {
const percent = Math.min(100, Math.max(0, Math.round(data.percent)));
if (progressPercent) progressPercent.textContent = `${percent}%`;
if (progressBarFill) progressBarFill.style.width = `${percent}%`;
if (progressBar) progressBar.style.width = `${percent}%`;
}
if (data.speed && data.downloaded && data.total) {
const speedMB = (data.speed / 1024 / 1024).toFixed(2);
const downloadedMB = (data.downloaded / 1024 / 1024).toFixed(2);
const totalMB = (data.total / 1024 / 1024).toFixed(2);
if (progressSpeed) progressSpeed.textContent = `${speedMB} MB/s`;
if (progressSize) progressSize.textContent = `${downloadedMB} / ${totalMB} MB`;
}
}
function setupAnimations() {
document.body.style.opacity = '0';
document.body.style.transform = 'translateY(20px)';
setTimeout(() => {
document.body.style.transition = 'all 0.6s ease';
document.body.style.opacity = '1';
document.body.style.transform = 'translateY(0)';
}, 100);
const style = document.createElement('style');
style.textContent = `
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`;
document.head.appendChild(style);
}
function setupFirstLaunchHandlers() {
console.log('Setting up first launch handlers...');
window.electronAPI.onFirstLaunchUpdate((data) => {
console.log('Received first launch update event:', data);
showFirstLaunchUpdateDialog(data);
});
window.electronAPI.onFirstLaunchWelcome(() => {
});
window.electronAPI.onFirstLaunchProgress((data) => {
showProgress();
updateProgress(data);
});
let lockButtonTimeout = null;
window.electronAPI.onLockPlayButton((locked) => {
lockPlayButton(locked);
if (locked) {
if (lockButtonTimeout) {
clearTimeout(lockButtonTimeout);
}
lockButtonTimeout = setTimeout(() => {
console.warn('Play button has been locked for too long, forcing unlock');
lockPlayButton(false);
lockButtonTimeout = null;
}, 20000);
} else {
if (lockButtonTimeout) {
clearTimeout(lockButtonTimeout);
lockButtonTimeout = null;
}
}
});
}
function showFirstLaunchUpdateDialog(data) {
console.log('Creating first launch modal...');
const existingModal = document.querySelector('.first-launch-modal-overlay');
if (existingModal) {
existingModal.remove();
}
const modalOverlay = document.createElement('div');
modalOverlay.className = 'first-launch-modal-overlay';
modalOverlay.style.cssText = `
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
background: rgba(0, 0, 0, 0.95) !important;
backdrop-filter: blur(10px) !important;
z-index: 999999 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
pointer-events: all !important;
`;
const modalDialog = document.createElement('div');
modalDialog.className = 'first-launch-modal-dialog';
modalDialog.style.cssText = `
background: #1a1a1a !important;
border-radius: 12px !important;
padding: 0 !important;
width: 500px !important;
max-width: 90vw !important;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8) !important;
border: 1px solid rgba(147, 51, 234, 0.5) !important;
overflow: hidden !important;
animation: modalSlideIn 0.3s ease-out !important;
`;
modalDialog.innerHTML = `
<div style="background: linear-gradient(135deg, rgba(147, 51, 234, 0.2), rgba(59, 130, 246, 0.2)); padding: 25px; border-bottom: 1px solid rgba(255,255,255,0.1);">
<h2 style="margin: 0; color: #fff; font-size: 1.5rem; font-weight: 600; text-align: center;">
🔄 Game Update Required
</h2>
</div>
<div style="padding: 30px; color: #e5e7eb; line-height: 1.6;">
<div style="text-align: center; margin-bottom: 25px;">
<p style="font-size: 1.1rem; margin-bottom: 15px;">
An existing Hytale installation has been detected and must be updated to the latest version.
</p>
<p style="color: #10b981; font-weight: 500; margin-bottom: 20px;">
✅ Your game saves and settings will be preserved
</p>
</div>
<div style="background: rgba(59, 130, 246, 0.1); padding: 20px; border-radius: 8px; border-left: 4px solid #3b82f6; margin: 20px 0;">
<p style="margin: 8px 0; font-family: 'Courier New', monospace; font-size: 0.9em;">
<strong>📁 Location:</strong> ${data.existingGame.installPath}
</p>
<p style="margin: 8px 0; font-family: 'Courier New', monospace; font-size: 0.9em;">
<strong>💾 UserData:</strong> ${data.existingGame.hasUserData ? '✅ Found (will be preserved)' : '❌ Not found'}
</p>
</div>
<div style="background: rgba(234, 179, 8, 0.1); padding: 15px; border-radius: 8px; border-left: 4px solid #eab308; margin: 20px 0;">
<p style="margin: 0; color: #fbbf24; font-weight: 500; font-size: 0.95em;">
⚠️ This update is mandatory and cannot be skipped
</p>
</div>
</div>
<div style="padding: 25px; border-top: 1px solid rgba(255,255,255,0.1); text-align: center;">
<button id="updateGameBtn" style="
background: linear-gradient(135deg, #9333ea, #3b82f6) !important;
color: white !important;
border: none !important;
padding: 15px 30px !important;
border-radius: 8px !important;
font-size: 1rem !important;
font-weight: 600 !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
min-width: 200px !important;
" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
🚀 Update Game Now
</button>
</div>
`;
modalOverlay.appendChild(modalDialog);
modalOverlay.onclick = (e) => {
if (e.target === modalOverlay) {
e.preventDefault();
e.stopPropagation();
return false;
}
};
document.addEventListener('keydown', function preventEscape(e) {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
return false;
}
});
document.body.appendChild(modalOverlay);
const updateBtn = document.getElementById('updateGameBtn');
updateBtn.onclick = () => {
acceptFirstLaunchUpdate();
};
window.firstLaunchExistingGame = data.existingGame;
console.log('First launch modal created and displayed');
}
function lockPlayButton(locked) {
const playButton = document.getElementById('homePlayBtn');
if (!playButton) {
console.warn('Play button not found');
return;
}
if (locked) {
playButton.style.opacity = '0.5';
playButton.style.pointerEvents = 'none';
playButton.style.cursor = 'not-allowed';
playButton.setAttribute('data-locked', 'true');
const spanElement = playButton.querySelector('span');
if (spanElement) {
if (!playButton.getAttribute('data-original-text')) {
playButton.setAttribute('data-original-text', spanElement.textContent);
}
spanElement.textContent = 'CHECKING...';
}
console.log('Play button locked');
} else {
playButton.style.opacity = '';
playButton.style.pointerEvents = '';
playButton.style.cursor = '';
playButton.removeAttribute('data-locked');
const spanElement = playButton.querySelector('span');
const originalText = playButton.getAttribute('data-original-text');
if (spanElement && originalText) {
spanElement.textContent = originalText;
playButton.removeAttribute('data-original-text');
}
console.log('Play button unlocked');
}
}
async function acceptFirstLaunchUpdate() {
const existingGame = window.firstLaunchExistingGame;
if (!existingGame) {
showNotification('Error: Game data not found', 'error');
return;
}
const modal = document.querySelector('.first-launch-modal-overlay');
if (modal) {
modal.style.pointerEvents = 'none';
const btn = document.getElementById('updateGameBtn');
if (btn) {
btn.style.opacity = '0.5';
btn.style.cursor = 'not-allowed';
btn.textContent = '🔄 Updating...';
}
}
try {
showProgress();
updateProgress({ message: 'Starting mandatory game update...', percent: 0 });
const result = await window.electronAPI.acceptFirstLaunchUpdate(existingGame);
window.electronAPI.markAsLaunched && window.electronAPI.markAsLaunched();
if (modal) {
modal.remove();
}
lockPlayButton(false);
if (result.success) {
hideProgress();
showNotification('Game updated successfully! 🎉', 'success');
} else {
hideProgress();
showNotification(`Update failed: ${result.error}`, 'error');
}
} catch (error) {
if (modal) {
modal.remove();
}
lockPlayButton(false);
hideProgress();
showNotification(`Update error: ${error.message}`, 'error');
}
}
function dismissFirstLaunchDialog() {
const modal = document.querySelector('.first-launch-modal-overlay');
if (modal) {
modal.remove();
}
lockPlayButton(false);
window.electronAPI.markAsLaunched && window.electronAPI.markAsLaunched();
}
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('show');
}, 100);
setTimeout(() => {
notification.remove();
}, 5000);
}
function setupUI() {
progressOverlay = document.getElementById('progressOverlay');
progressBar = document.getElementById('progressBar');
progressBarFill = document.getElementById('progressBarFill');
progressText = document.getElementById('progressText');
progressPercent = document.getElementById('progressPercent');
progressSpeed = document.getElementById('progressSpeed');
progressSize = document.getElementById('progressSize');
lockPlayButton(true);
setTimeout(() => {
const playButton = document.getElementById('homePlayBtn');
if (playButton && playButton.getAttribute('data-locked') === 'true') {
const spanElement = playButton.querySelector('span');
if (spanElement && spanElement.textContent === 'CHECKING...') {
console.warn('Play button still locked after startup timeout, forcing unlock');
lockPlayButton(false);
}
}
}, 25000);
handleNavigation();
setupWindowControls();
setupSidebarLogo();
setupAnimations();
setupFirstLaunchHandlers();
document.body.focus();
}
window.LauncherUI = {
showPage,
setActiveNav,
showLauncherOrInstall,
showProgress,
hideProgress,
updateProgress
};
document.addEventListener('DOMContentLoaded', setupUI);

View File

@@ -1,162 +0,0 @@
class ClientUpdateManager {
constructor() {
this.updatePopupVisible = false;
this.init();
}
init() {
window.electronAPI.onUpdatePopup((updateInfo) => {
this.showUpdatePopup(updateInfo);
});
this.checkForUpdatesOnDemand();
}
showUpdatePopup(updateInfo) {
if (this.updatePopupVisible) return;
this.updatePopupVisible = true;
const popupHTML = `
<div id="update-popup-overlay">
<div class="update-popup-container update-popup-pulse">
<div class="update-popup-header">
<div class="update-popup-icon">
<i class="fas fa-download"></i>
</div>
<h2 class="update-popup-title">
NEW UPDATE AVAILABLE
</h2>
</div>
<div class="update-popup-versions">
<div class="version-row">
<span class="version-label">Current Version:</span>
<span class="version-current">${updateInfo.currentVersion}</span>
</div>
<div class="version-row">
<span class="version-label">New Version:</span>
<span class="version-new">${updateInfo.newVersion}</span>
</div>
</div>
<div class="update-popup-message">
A new version of Hytale F2P Launcher is available.<br>
Please download the latest version to continue using the launcher.
</div>
<button id="update-download-btn" class="update-download-btn">
<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>
Download Update
</button>
<div class="update-popup-footer">
This popup cannot be closed until you update the launcher
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', popupHTML);
this.blockInterface();
const downloadBtn = document.getElementById('update-download-btn');
if (downloadBtn) {
downloadBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
downloadBtn.disabled = true;
downloadBtn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right: 0.5rem;"></i>Opening GitHub...';
try {
await window.electronAPI.openDownloadPage();
console.log('✅ Download page opened, launcher will close...');
downloadBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Launcher closing...';
} catch (error) {
console.error('❌ Error opening download page:', error);
downloadBtn.disabled = false;
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Update';
}
});
}
const overlay = document.getElementById('update-popup-overlay');
if (overlay) {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
e.preventDefault();
e.stopPropagation();
return false;
}
});
}
console.log('🔔 Update popup displayed with new style');
}
blockInterface() {
const mainContent = document.querySelector('.flex.w-full.h-screen');
if (mainContent) {
mainContent.classList.add('interface-blocked');
}
document.body.classList.add('no-select');
document.addEventListener('keydown', this.blockKeyEvents.bind(this), true);
document.addEventListener('contextmenu', this.blockContextMenu.bind(this), true);
console.log('🚫 Interface blocked for update');
}
blockKeyEvents(event) {
if (event.target.closest('#update-popup-overlay')) {
if ((event.key === 'Enter' || event.key === ' ') &&
event.target.id === 'update-download-btn') {
return;
}
if (event.key !== 'Tab') {
event.preventDefault();
event.stopPropagation();
}
return;
}
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
return false;
}
blockContextMenu(event) {
if (!event.target.closest('#update-popup-overlay')) {
event.preventDefault();
event.stopPropagation();
return false;
}
}
async checkForUpdatesOnDemand() {
try {
const updateInfo = await window.electronAPI.checkForUpdates();
if (updateInfo.updateAvailable) {
this.showUpdatePopup(updateInfo);
}
return updateInfo;
} catch (error) {
console.error('Error checking for updates:', error);
return { updateAvailable: false, error: error.message };
}
}
}
document.addEventListener('DOMContentLoaded', () => {
window.updateManager = new ClientUpdateManager();
});
window.ClientUpdateManager = ClientUpdateManager;

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +0,0 @@
[Desktop Entry]
Type=Application
Name=Hytale-F2P
Comment=A modern, cross-platform launcher for Hytale with automatic updates and multi-client support
Exec=/opt/Hytale-F2P/hytale-f2p-launcherv2
Categories=Game;
Icon=Hytale-F2P
Terminal=false
StartupNotify=true

View File

@@ -1,30 +0,0 @@
# Maintainer: Terromur <terromuroz@proton.me>
pkgname=Hytale-F2P-git
_pkgname=Hytale-F2P
pkgver=2.0.2.r90.g21f8527
pkgrel=1
pkgdesc="HyLauncher - unofficial Hytale Launcher for free to play gamers"
arch=('x86_64')
url="https://github.com/amiayweb/Hytale-F2P"
license=('custom')
makedepends=('npm')
source=("git+$url.git" "Hytale-F2P.desktop")
sha256sums=('SKIP' '8c78a6931fade2b0501122980dc238e042b9f6f0292b5ca74c391d7b3c1543c0')
pkgver() {
cd "$_pkgname"
printf "2.0.2.r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
build() {
cd "$_pkgname"
npm install
npm run build:linux
}
package() {
mkdir -p "$pkgdir/opt/$_pkgname"
cp -r "$_pkgname/dist/linux-unpacked/"* "$pkgdir/opt/$_pkgname"
install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
install -Dm644 "$_pkgname/icon.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/$_pkgname.png"
}

207
README.md
View File

@@ -1,206 +1,7 @@
# 🎮 Hytale F2P Launcher | Multiplayer Support [Windows, MacOS, Linux] This project has been taken down by poor hypixel studios.
<div align="center"> We still owning the HF2P community this is not illegal so cry.
![Version](https://img.shields.io/badge/Version-2.0.2-green?style=for-the-badge)
![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey?style=for-the-badge)
![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge)
**A modern, cross-platform offline launcher for Hytale with automatic updates and multiplayer support (all OS supported)**
[![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!**
🛑 **Found a problem? Join the Discord: https://discord.gg/gME8rUy3MB** 🛑
</div>
---
## 📸 Screenshots
<div align="center">
![Hytale F2P Launcher](https://i.imgur.com/9iDuzST.png)
![Hytale F2P Mods](https://i.imgur.com/NaareIS.png)
![Hytale F2P News](https://i.imgur.com/n1nEqRS.png)
![Hytale F2P Chat](https://i.imgur.com/Y4hL3sx.png)
</div>
---
## ✨ Features
🎯 **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
-**Java Management** - Automatic Java runtime detection and installation
- 🎮 **Multiplayer Support** - Automatic multiplayer client installation (Windows, macOS & Linux !)
🛡️ **Advanced Features**
- 📁 **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
---
## 🚀 Quick Start
### 📥 Installation
#### Windows
1. Download the latest `Hytale-F2P.exe` from [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases)
2. Run the installer
3. Launch from desktop or start menu
#### Linux
See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases) section.
#### macOS
See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases) section.
#### 🖥️ How to play online on F2P?
See [SERVER.md](SERVER.md)
---
## 🛠️ Building from Source
See [BUILD.md](BUILD.md) for comprehensive build instructions.
---
## 📋 Changelog
### 🆕 v2.0.2 *(Latest)*
- 🎮 **Discord RPC Integration** - Added Discord Rich Presence with toggle in settings (enabled by default)
- 🌐 **Cross-Platform Multiplayer** - Added multiplayer patch support for Windows, Linux, and macOS
- 🎨 **Chat Improvements** - Simplified chat color system
- 🏆 **Badge System Expansion** - Added new FOUNDER UUID to the badge system
- 🔧 **Progress Bar Fix** - Resolved issue where download progress bar stayed active after game launch
- 🐛 **Bug Fixes**: General fixes
### 🔄 v2.0.1
- 📊 **Advanced Logging System** - Complete logging with timestamps, file rotation, and session tracking
- 🔧 **Play Button Fix** - Resolved issue where play button could get stuck in "CHECKING..." state
- 💬 **Discord Integration** - Added closable Discord notification for community engagement
- 📁 **Game Location Access** - New "Open Game Location" button in settings for easy file access
- 🎯 **UI Polish** - Removed bounce animation from player counter for smoother experience
- 🛡️ **Stability Improvements** - Enhanced error handling and process lifecycle management
-**Performance Optimizations** - Faster startup times and better resource management
- 🔄 **Timeout Protection** - Added safety timeouts to prevent launcher freezing
### 🔄 v2.0.0
-**Automatic Game Update System** - Smart version checking and seamless updates
-**Partial Automatic Launcher Update System** - This will inform you when I release a new update.
- 🛡️ **UserData Preservation** - Intelligent backup/restore of game saves during updates
- 🐧 **Enhanced Linux Support** - Full Wayland and X11 compatibility
- 🔄 **Multiplayer Auto-Install** - Automatic multiplayer client setup on updates (Windows)
- 📡 **API Integration** - Real-time version checking and client management
- 🎨 **UI Improvements** - Added contributor credits footer
- 🔄 **Complete Launcher Overhaul** - Total redesign of the launcher architecture and interface
- 🗂️ **Integrated Mod Manager** - Built-in mod installation, management
- 💬 **Community Chat System** - Real-time chat for launcher users to connect and communicate
### 🔧 v1.0.1
- 📁 **Custom Installation** - Choose installation directory with file browser
- 🏠 **Always on Top** - Launcher stays visible during installation
- 🧠 **Smart Detection** - Automatic game detection and UI adaptation
- 🗑️ **Uninstall Feature** - Easy game removal with one click
- 🔄 **Dynamic UI** - "INSTALL" vs "PLAY" button based on game state
- 🛠️ **Path Management** - Proper custom directory handling
- 💫 **UI Polish** - Improved layout and overflow prevention
### 🎉 v1.0.0 *(Initial Release)*
- 🎮 **Offline Gameplay** - Play Hytale without internet connection
-**Auto Installation** - One-click game setup
-**Java Management** - Automatic Java runtime handling
- 🎨 **Modern Interface** - Clean, intuitive design
- 🌟 **First Release** - Core launcher functionality
---
## 👥 Contributors
<div align="center">
**Made with ❤️ by the community**
[![Contributors](https://contrib.rocks/image?repo=amiayweb/Hytale-F2P)](https://github.com/amiayweb/Hytale-F2P/graphs/contributors)
</div>
### 🏆 Project Creator
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator*
- [**@Relyz1993**](https://github.com/Relyz1993) - *Server Helper & Second Developer & Project Creator*
### 🌟 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*
- [**@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*
---
## 📊 GitHub Stats
<div align="center">
![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=for-the-badge&logo=github)
![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=for-the-badge&logo=github)
![GitHub issues](https://img.shields.io/github/issues/amiayweb/Hytale-F2P?style=for-the-badge&logo=github)
![GitHub downloads](https://img.shields.io/github/downloads/amiayweb/Hytale-F2P/total?style=for-the-badge&logo=github)
</div>
## 📞 Support
<div align="center">
**Need help?** Join us: https://discord.gg/gME8rUy3MB
</div>
---
## ⚖️ Legal Disclaimer
<div align="center">
⚠️ **Important Notice** ⚠️
</div>
This launcher is created for **educational purposes only**.
🏛️ **Not Official** - This is an independent fan project **not affiliated with, endorsed by, or associated with** Hypixel Studios or Hytale.
🛡️ **No Warranty** - This software is provided **"as is"** without any warranty of any kind.
📝 **Responsibility** - The authors take no responsibility for how this software is used.
🛑 **Takedown Policy** - If Hypixel Studios or Hytale requests removal, this project will be taken down immediately.
❤️ **Support Official** - Please support the official game by purchasing it when available.
---
<div align="center">
**⭐ Star this project if you found it helpful! ⭐**
*Made with ❤️ by [@amiayweb](https://github.com/amiayweb) and the amazing community*
[![Star History Chart](https://api.star-history.com/svg?repos=amiayweb/Hytale-F2P&type=date&legend=top-left)](https://www.star-history.com/#amiayweb/Hytale-F2P&type=date&legend=top-left)
</div>
https://discord.gg/mzdfCJy5J
https://t.me/hf2p_og

444
SERVER.md
View File

@@ -1,444 +0,0 @@
# 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
---
## Part 1: Playing with Friends (Online Play)
The easiest way to play with friends - no manual server setup required!
### 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:
- **UPnP enabled** on your router (most routers have this on by default)
- **Public IP address** from your ISP (not behind CGNAT)
### Common Issues
#### "NAT Type: Carrier-Grade NAT (CGNAT)" Warning
If you see this message:
```
Connected via UPnP
NAT Type: Carrier-Grade NAT (CGNAT)
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)
4. **Have a friend with public IP host instead**
#### "UPnP Failed" or "Port Mapping Failed"
**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")
3. Enable UPnP if disabled
4. Restart your router
**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"
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
### Workarounds for NAT/CGNAT Issues
#### Option 1: 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
Works with both Online Play and dedicated servers. No software needed for players joining.
#### Option 2: 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
Both options bypass all NAT/CGNAT issues.
---
## Part 2: Dedicated Server (Advanced)
For 24/7 servers, custom configurations, or hosting on a VPS/dedicated machine.
### Quick Start
#### Step 1: Get the Server JAR
The server scripts will automatically download the pre-patched server JAR if it's not present.
**Option A:** Let the scripts download automatically (requires `HYTALE_SERVER_URL` to be configured)
**Option B:** Manually place `HytaleServer.jar` (pre-patched for F2P) in the Server directory:
- **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: 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)
### 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)
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
**Windows Firewall:**
```powershell
# Run as Administrator
netsh advfirewall firewall add rule name="Hytale Server" dir=in action=allow protocol=UDP localport=5520
```
---
## Configuration
### Environment Variables
Set these before running to customize your server:
| Variable | Default | Description |
|----------|---------|-------------|
| `HYTALE_SERVER_URL` | (placeholder) | URL to download pre-patched server JAR |
| `HYTALE_AUTH_DOMAIN` | `sanasol.ws` | Auth server domain |
| `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 |
| `JVM_XMS` | `2G` | Minimum Java memory |
| `JVM_XMX` | `4G` | Maximum Java memory |
**Example (Windows):**
```batch
set HYTALE_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
```
### Authentication Modes
| Mode | Description | Use Case |
|------|-------------|----------|
| `authenticated` | Players log in via F2P Launcher | Public servers |
| `unauthenticated` | No login required | LAN parties, testing |
| `singleplayer` | Local play only | Solo testing |
---
## RAM Allocation Guide
Adjust memory based on your system:
| System RAM | Players | JVM_XMS | JVM_XMX |
|------------|---------|---------|---------|
| 4 GB | 1-3 | `512M` | `2G` |
| 8 GB | 3-8 | `1G` | `4G` |
| 16 GB | 8-15 | `2G` | `8G` |
| 32 GB | 15+ | `4G` | `12G` |
**Example for large server:**
```bash
JVM_XMS=4G JVM_XMX=12G ./run_server.sh
```
**Tips:**
- `-Xms` = minimum RAM (allocated at startup)
- `-Xmx` = maximum RAM (upper limit)
- Never allocate all your system RAM - leave room for OS
- Start conservative and increase if needed
---
## Server Commands
Once running, use these commands in the console:
| Command | Description |
|---------|-------------|
| `help` | Show all commands |
| `stop` | Stop server gracefully |
| `save` | Force world save |
| `list` | List online players |
| `op <player>` | Give operator status |
| `deop <player>` | Remove operator status |
| `kick <player>` | Kick a player |
| `ban <player>` | Ban a player |
| `unban <player>` | Unban a player |
| `tp <player> <x> <y> <z>` | Teleport player |
---
## Command Line Options
Pass these when starting the server:
```bash
./run_server.sh [OPTIONS]
```
| Option | Description |
|--------|-------------|
| `--bind <ip:port>` | Server address (default: 0.0.0.0:5520) |
| `--auth-mode <mode>` | Authentication mode |
| `--universe <path>` | Path to world data |
| `--mods <path>` | Path to mods folder |
| `--backup` | Enable automatic backups |
| `--backup-dir <path>` | Backup directory |
| `--backup-frequency <mins>` | Backup interval |
| `--owner-name <name>` | Server owner username |
| `--allow-op` | Allow op commands |
| `--disable-sentry` | Disable crash reporting |
| `--help` | Show all options |
**Example:**
```bash
./run_server.sh --backup --backup-frequency 30 --allow-op
```
---
## File Structure
```
<game_path>/
├── Assets.zip # Game assets (required)
├── Client/ # Game client
└── Server/
├── HytaleServer.jar # Server executable (pre-patched)
├── HytaleServer.aot # AOT cache (faster startup)
├── universe/ # World data
│ ├── world/ # Default world
│ └── players/ # Player data
├── mods/ # Server mods (optional)
└── Licenses/ # License files
```
---
## Backups
### Automatic Backups
```bash
./run_server.sh --backup --backup-dir ./backups --backup-frequency 30
```
### Manual Backup
1. Use `save` command or stop the server
2. Copy the `universe/` folder
3. Store in a safe location
### Restore
1. Stop the server
2. Delete/rename current `universe/`
3. Copy backup to `universe/`
4. Restart server
---
## Troubleshooting
### "Java not found" or "Java version too old"
**Java 21 is REQUIRED** (the server uses class file version 65.0).
**Install Java 21:**
- **Windows:** `winget install EclipseAdoptium.Temurin.21.JDK`
- **macOS:** `brew install openjdk@21`
- **Ubuntu:** `sudo apt install openjdk-21-jdk`
- **Fedora:** `sudo dnf install java-21-openjdk`
- **Arch:** `sudo pacman -S jdk21-openjdk`
- **Download:** [adoptium.net/temurin/releases/?version=21](https://adoptium.net/temurin/releases/?version=21)
**macOS: Set Java 21 as default:**
```bash
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
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"
```bash
./run_server.sh --bind 0.0.0.0:5521
```
### "Out of memory"
Increase JVM_XMX:
```bash
JVM_XMX=6G ./run_server.sh
```
### Players can't connect
1. Server shows "Server Ready"?
2. Using F2P Launcher (not official)?
3. Port 5520 open in firewall?
4. Port forwarding configured (for internet)?
5. Try `--auth-mode unauthenticated` for testing
### "Authentication failed"
- Ensure players use F2P Launcher
- Auth server may be temporarily down
- Test with `--auth-mode unauthenticated`
---
## Docker Deployment (Advanced)
For production servers, use Docker:
```bash
docker run -d \
--name hytale-server \
-p 5520:5520/udp \
-v ./data:/data \
-e HYTALE_AUTH_DOMAIN=sanasol.ws \
-e HYTALE_SERVER_NAME="My Server" \
-e JVM_XMX=8G \
ghcr.io/hybrowse/hytale-server-docker:latest
```
See [Docker documentation](https://github.com/Hybrowse/hytale-server-docker) for details.
---
## 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
- Check server console logs for errors
- Test with `--auth-mode unauthenticated` first
- Ensure all players have F2P Launcher
- Join the community for support
---
## Credits
- Hytale F2P Project
- [Hybrowse Docker Image](https://github.com/Hybrowse/hytale-server-docker)
- Auth Server: sanasol.ws

View File

@@ -1,312 +0,0 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
// Default auth domain - can be overridden by env var or config
const DEFAULT_AUTH_DOMAIN = 'sanasol.ws';
// Get auth domain from env, config, or default
function getAuthDomain() {
// First check environment variable
if (process.env.HYTALE_AUTH_DOMAIN) {
return process.env.HYTALE_AUTH_DOMAIN;
}
// Then check config file
const config = loadConfig();
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
// Allow profile to override auth domain if ever needed
// but for now stick to global or env
}
if (config.authDomain) {
return config.authDomain;
}
// Fall back to default
return DEFAULT_AUTH_DOMAIN;
}
// Get full auth server URL
function getAuthServerUrl() {
const domain = getAuthDomain();
return `https://sessions.${domain}`;
}
// Save auth domain to config
function saveAuthDomain(domain) {
saveConfig({ authDomain: domain || DEFAULT_AUTH_DOMAIN });
}
function getAppDir() {
const home = os.homedir();
if (process.platform === 'win32') {
return path.join(home, 'AppData', 'Local', 'HytaleF2P');
} else if (process.platform === 'darwin') {
return path.join(home, 'Library', 'Application Support', 'HytaleF2P');
} else {
return path.join(home, '.hytalef2p');
}
}
const CONFIG_FILE = path.join(getAppDir(), 'config.json');
function loadConfig() {
try {
if (fs.existsSync(CONFIG_FILE)) {
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
}
} catch (err) {
console.log('Notice: could not load config:', err.message);
}
return {};
}
function saveConfig(update) {
try {
const configDir = path.dirname(CONFIG_FILE);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
const config = loadConfig();
const next = { ...config, ...update };
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
} catch (err) {
console.log('Notice: could not save config:', err.message);
}
}
function saveUsername(username) {
saveConfig({ username: username || 'Player' });
}
function loadUsername() {
const config = loadConfig();
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();
const userUuids = config.userUuids || {};
if (userUuids[username]) {
return userUuids[username];
}
const newUuid = uuidv4();
userUuids[username] = newUuid;
saveConfig({ userUuids });
return newUuid;
}
function saveJavaPath(javaPath) {
const trimmed = (javaPath || '').trim();
saveConfig({ javaPath: trimmed });
}
function loadJavaPath() {
const config = loadConfig();
// Prefer Active Profile's Java Path
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
const profile = config.profiles[config.activeProfileId];
if (profile.javaPath && profile.javaPath.trim().length > 0) {
return profile.javaPath;
}
}
// Fallback to global setting
return config.javaPath || '';
}
function saveInstallPath(installPath) {
const trimmed = (installPath || '').trim();
saveConfig({ installPath: trimmed });
}
function loadInstallPath() {
const config = loadConfig();
return config.installPath || '';
}
function saveDiscordRPC(enabled) {
saveConfig({ discordRPC: !!enabled });
}
function loadDiscordRPC() {
const config = loadConfig();
return config.discordRPC !== undefined ? config.discordRPC : true;
}
function saveModsToConfig(mods) {
try {
const config = loadConfig();
// Config migration handles structure, but mod saves must go to the ACTIVE profile.
// Global installedMods is kept mainly for reference/migration.
// The profile is the source of truth for enabled mods.
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
config.profiles[config.activeProfileId].mods = mods;
} else {
// Fallback for legacy or no-profile state
config.installedMods = mods;
}
const configDir = path.dirname(CONFIG_FILE);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
console.log('Mods saved to config.json');
} catch (error) {
console.error('Error saving mods to config:', error);
}
}
function loadModsFromConfig() {
try {
const config = loadConfig();
// Prefer Active Profile
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
return config.profiles[config.activeProfileId].mods || [];
}
return config.installedMods || [];
} catch (error) {
console.error('Error loading mods from config:', error);
return [];
}
}
function isFirstLaunch() {
const config = loadConfig();
if ('hasLaunchedBefore' in config) {
return !config.hasLaunchedBefore;
}
const hasUserData = config.installPath || config.username || config.javaPath ||
config.chatUsername || config.userUuids ||
Object.keys(config).length > 0;
if (!hasUserData) {
return true;
}
return true;
}
function markAsLaunched() {
saveConfig({ hasLaunchedBefore: true, firstLaunchDate: new Date().toISOString() });
}
// UUID Management Functions
function getCurrentUuid() {
const username = loadUsername();
return getUuidForUser(username);
}
function getAllUuidMappings() {
const config = loadConfig();
return config.userUuids || {};
}
function setUuidForUser(username, uuid) {
const { v4: uuidv4, validate: validateUuid } = require('uuid');
// Validate UUID format
if (!validateUuid(uuid)) {
throw new Error('Invalid UUID format');
}
const config = loadConfig();
const userUuids = config.userUuids || {};
userUuids[username] = uuid;
saveConfig({ userUuids });
return uuid;
}
function generateNewUuid() {
const { v4: uuidv4 } = require('uuid');
return uuidv4();
}
function deleteUuidForUser(username) {
const config = loadConfig();
const userUuids = config.userUuids || {};
if (userUuids[username]) {
delete userUuids[username];
saveConfig({ userUuids });
return true;
}
return false;
}
function resetCurrentUserUuid() {
const username = loadUsername();
const { v4: uuidv4 } = require('uuid');
const newUuid = uuidv4();
return setUuidForUser(username, newUuid);
}
function saveChatColor(color) {
const config = loadConfig();
config.chatColor = color;
saveConfig(config);
}
function loadChatColor() {
const config = loadConfig();
return config.chatColor || '#3498db';
}
module.exports = {
loadConfig,
saveConfig,
saveUsername,
loadUsername,
saveChatUsername,
loadChatUsername,
saveChatColor,
loadChatColor,
getUuidForUser,
saveJavaPath,
loadJavaPath,
saveInstallPath,
loadInstallPath,
saveDiscordRPC,
loadDiscordRPC,
saveModsToConfig,
loadModsFromConfig,
isFirstLaunch,
markAsLaunched,
CONFIG_FILE,
// Auth server exports
getAuthServerUrl,
getAuthDomain,
saveAuthDomain,
// UUID Management exports
getCurrentUuid,
getAllUuidMappings,
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
resetCurrentUserUuid
};

View File

@@ -1,195 +0,0 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
function getAppDir() {
const home = os.homedir();
if (process.platform === 'win32') {
return path.join(home, 'AppData', 'Local', 'HytaleF2P');
} else if (process.platform === 'darwin') {
return path.join(home, 'Library', 'Application Support', 'HytaleF2P');
} else {
return path.join(home, '.hytalef2p');
}
}
const DEFAULT_APP_DIR = getAppDir();
function getResolvedAppDir(customPath) {
if (customPath && customPath.trim()) {
return path.join(customPath.trim(), 'HytaleF2P');
}
try {
const configFile = path.join(DEFAULT_APP_DIR, 'config.json');
if (fs.existsSync(configFile)) {
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
if (config.installPath && config.installPath.trim()) {
return path.join(config.installPath.trim(), 'HytaleF2P');
}
}
} catch (err) {
}
return DEFAULT_APP_DIR;
}
function expandHome(inputPath) {
if (!inputPath) {
return inputPath;
}
if (inputPath === '~') {
return os.homedir();
}
if (inputPath.startsWith('~/') || inputPath.startsWith('~\\')) {
return path.join(os.homedir(), inputPath.slice(2));
}
return inputPath;
}
const APP_DIR = DEFAULT_APP_DIR;
const CACHE_DIR = path.join(APP_DIR, 'cache');
const TOOLS_DIR = path.join(APP_DIR, 'butler');
const GAME_DIR = path.join(APP_DIR, 'release', 'package', 'game', 'latest');
const JRE_DIR = path.join(APP_DIR, 'release', 'package', 'jre', 'latest');
const PLAYER_ID_FILE = path.join(APP_DIR, 'player_id.json');
function getClientCandidates(gameLatest) {
const candidates = [];
if (process.platform === 'win32') {
candidates.push(path.join(gameLatest, 'Client', 'HytaleClient.exe'));
} else if (process.platform === 'darwin') {
candidates.push(path.join(gameLatest, 'Client', 'Hytale.app', 'Contents', 'MacOS', 'HytaleClient'));
candidates.push(path.join(gameLatest, 'Client', 'HytaleClient'));
} else {
candidates.push(path.join(gameLatest, 'Client', 'HytaleClient'));
}
return candidates;
}
function findClientPath(gameLatest) {
const candidates = getClientCandidates(gameLatest);
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
function findUserDataPath(gameLatest) {
const candidates = [];
candidates.push(path.join(gameLatest, 'Client', 'UserData'));
candidates.push(path.join(gameLatest, 'Client', 'Hytale.app', 'Contents', 'UserData'));
candidates.push(path.join(gameLatest, 'Hytale.app', 'Contents', 'UserData'));
candidates.push(path.join(gameLatest, 'UserData'));
candidates.push(path.join(gameLatest, 'Client', 'UserData'));
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
let defaultPath;
if (process.platform === 'darwin') {
defaultPath = path.join(gameLatest, 'Client', 'UserData');
} else {
defaultPath = path.join(gameLatest, 'Client', 'UserData');
}
if (!fs.existsSync(defaultPath)) {
fs.mkdirSync(defaultPath, { recursive: true });
}
return defaultPath;
}
function findUserDataRecursive(gameLatest) {
function searchDirectory(dir) {
try {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
if (item.isDirectory()) {
const fullPath = path.join(dir, item.name);
if (item.name === 'UserData') {
return fullPath;
}
const found = searchDirectory(fullPath);
if (found) {
return found;
}
}
}
} catch (error) {
}
return null;
}
if (!fs.existsSync(gameLatest)) {
return null;
}
const found = searchDirectory(gameLatest);
return found;
}
async function getModsPath(customInstallPath = null) {
try {
let installPath = customInstallPath;
if (!installPath) {
const configFile = path.join(DEFAULT_APP_DIR, 'config.json');
if (fs.existsSync(configFile)) {
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
installPath = config.installPath || '';
}
}
if (!installPath) {
// Use the standard app directory logic which handles platforms correctly
installPath = getAppDir();
}
const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest');
const userDataPath = findUserDataPath(gameLatest);
const modsPath = path.join(userDataPath, 'Mods');
const disabledModsPath = path.join(userDataPath, 'DisabledMods');
if (!fs.existsSync(modsPath)) {
fs.mkdirSync(modsPath, { recursive: true });
}
if (!fs.existsSync(disabledModsPath)) {
fs.mkdirSync(disabledModsPath, { recursive: true });
}
return modsPath;
} catch (error) {
console.error('Error getting mods path:', error);
throw error;
}
}
module.exports = {
getAppDir,
getResolvedAppDir,
expandHome,
APP_DIR,
CACHE_DIR,
TOOLS_DIR,
GAME_DIR,
JRE_DIR,
PLAYER_ID_FILE,
getClientCandidates,
findClientPath,
findUserDataPath,
findUserDataRecursive,
getModsPath
};

View File

@@ -1,156 +0,0 @@
// Main launcher module - orchestrates all launcher functionality
// This file serves as the main entry point and re-exports all necessary functions
// Core modules
const {
saveUsername,
loadUsername,
saveChatUsername,
loadChatUsername,
saveChatColor,
loadChatColor,
saveJavaPath,
loadJavaPath,
saveInstallPath,
loadInstallPath,
saveDiscordRPC,
loadDiscordRPC,
saveModsToConfig,
loadModsFromConfig,
getUuidForUser,
isFirstLaunch,
markAsLaunched,
CONFIG_FILE,
// UUID Management
getCurrentUuid,
getAllUuidMappings,
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
resetCurrentUserUuid
} = require('./core/config');
const { getResolvedAppDir, getModsPath } = require('./core/paths');
// Managers
const {
isGameInstalled,
installGame,
uninstallGame,
updateGameFiles,
checkExistingGameInstallation
} = require('./managers/gameManager');
const {
launchGame,
launchGameWithVersionCheck
} = require('./managers/gameLauncher');
const { getJavaDetection } = require('./managers/javaManager');
const {
downloadAndReplaceHomePageUI,
findHomePageUIPath,
downloadAndReplaceLogo,
findLogoPath
} = require('./managers/uiFileManager');
const {
loadInstalledMods,
downloadMod,
uninstallMod,
toggleMod
} = require('./managers/modManager');
// Services
const {
getInstalledClientVersion,
getLatestClientVersion
} = require('./services/versionManager');
const { getHytaleNews } = require('./services/newsManager');
const { getOrCreatePlayerId } = require('./services/playerManager');
const {
proposeGameUpdate,
handleFirstLaunchCheck
} = require('./services/firstLaunch');
// Re-export all functions to maintain backward compatibility
module.exports = {
// Game launch functions
launchGame,
launchGameWithVersionCheck,
// Game installation functions
installGame,
isGameInstalled,
uninstallGame,
updateGameFiles,
// User configuration functions
saveUsername,
loadUsername,
saveChatUsername,
loadChatUsername,
saveChatColor,
loadChatColor,
getUuidForUser,
// Java configuration functions
saveJavaPath,
loadJavaPath,
getJavaDetection,
// Installation path functions
saveInstallPath,
loadInstallPath,
// Discord RPC functions
saveDiscordRPC,
loadDiscordRPC,
// Version functions
getInstalledClientVersion,
getLatestClientVersion,
// News functions
getHytaleNews,
// Player ID functions
getOrCreatePlayerId,
// UUID Management functions
getCurrentUuid,
getAllUuidMappings,
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
resetCurrentUserUuid,
// Mod management functions
getModsPath,
loadInstalledMods,
downloadMod,
uninstallMod,
toggleMod,
saveModsToConfig,
loadModsFromConfig,
// UI file management functions
downloadAndReplaceHomePageUI,
findHomePageUIPath,
downloadAndReplaceLogo,
findLogoPath,
// First launch functions
isFirstLaunch,
markAsLaunched,
checkExistingGameInstallation,
proposeGameUpdate,
handleFirstLaunchCheck,
// Path functions
getResolvedAppDir
};

View File

@@ -1,213 +0,0 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
class Logger {
constructor() {
this.logDir = null;
this.logFile = null;
this.maxLogSize = 10 * 1024 * 1024; // 10MB
this.maxLogFiles = 5;
this.originalConsole = {
log: console.log,
error: console.error,
warn: console.warn,
info: console.info
};
this.initializeLogDirectory();
}
getAppDir() {
const home = os.homedir();
if (process.platform === 'win32') {
return path.join(home, 'AppData', 'Local', 'HytaleF2P');
} else if (process.platform === 'darwin') {
return path.join(home, 'Library', 'Application Support', 'HytaleF2P');
} else {
return path.join(home, '.hytalef2p');
}
}
getInstallPath() {
try {
const configFile = path.join(this.getAppDir(), 'config.json');
if (fs.existsSync(configFile)) {
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
if (config.installPath && config.installPath.trim()) {
return path.join(config.installPath.trim(), 'HytaleF2P');
}
}
} catch (err) {
}
return this.getAppDir();
}
initializeLogDirectory() {
try {
const installPath = this.getInstallPath();
this.logDir = path.join(installPath, 'logs');
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
const today = new Date();
const dateString = today.toISOString().split('T')[0]; // YYYY-MM-DD
const timeString = today.toISOString().split('T')[1].split('.')[0].replace(/:/g, '-'); // HH-MM-SS
this.logFile = path.join(this.logDir, `launcher-${dateString}-${timeString}.log`);
this.writeToFile(`\n=== NEW LAUNCHER SESSION - ${today.toISOString()} ===\n`);
} catch (error) {
this.logDir = path.join(os.tmpdir(), 'HytaleF2P-logs');
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
const today = new Date();
const dateString = today.toISOString().split('T')[0];
const timeString = today.toISOString().split('T')[1].split('.')[0].replace(/:/g, '-');
this.logFile = path.join(this.logDir, `launcher-${dateString}-${timeString}.log`);
this.writeToFile(`\n=== FALLBACK SESSION IN TEMP - ${today.toISOString()} ===\n`);
}
}
writeToFile(message) {
if (!this.logFile) return;
try {
if (fs.existsSync(this.logFile)) {
const stats = fs.statSync(this.logFile);
if (stats.size > this.maxLogSize) {
this.rotateLogFile();
}
}
fs.appendFileSync(this.logFile, message, 'utf8');
} catch (error) {
this.originalConsole.error('Impossible d\'écrire dans le fichier de log:', error.message);
}
}
rotateLogFile() {
try {
const today = new Date();
const dateString = today.toISOString().split('T')[0];
const timeString = today.toISOString().split('T')[1].split('.')[0].replace(/:/g, '-');
const rotatedFile = path.join(this.logDir, `launcher-${dateString}-${timeString}.log`);
fs.renameSync(this.logFile, rotatedFile);
this.cleanupOldLogs();
const newToday = new Date();
const newDateString = newToday.toISOString().split('T')[0];
const newTimeString = newToday.toISOString().split('T')[1].split('.')[0].replace(/:/g, '-');
this.logFile = path.join(this.logDir, `launcher-${newDateString}-${newTimeString}.log`);
this.writeToFile(`\n=== LOG ROTATION - ${newToday.toISOString()} ===\n`);
} catch (error) {
this.originalConsole.error('Erreur lors de la rotation des logs:', error.message);
}
}
cleanupOldLogs() {
try {
const files = fs.readdirSync(this.logDir)
.filter(file => file.startsWith('launcher-') && file.endsWith('.log'))
.map(file => ({
name: file,
path: path.join(this.logDir, file),
mtime: fs.statSync(path.join(this.logDir, file)).mtime
}))
.sort((a, b) => b.mtime - a.mtime);
if (files.length > this.maxLogFiles) {
const filesToDelete = files.slice(this.maxLogFiles);
filesToDelete.forEach(file => {
try {
fs.unlinkSync(file.path);
} catch (err) {
this.originalConsole.error(`Impossible de supprimer le fichier de log ${file.name}:`, err.message);
}
});
}
} catch (error) {
this.originalConsole.error('Erreur lors du nettoyage des logs:', error.message);
}
}
formatLogMessage(level, ...args) {
const timestamp = new Date().toISOString();
const message = args.map(arg => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg, null, 2);
} catch (e) {
return String(arg);
}
}
return String(arg);
}).join(' ');
return `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
}
log(...args) {
const logMessage = this.formatLogMessage('info', ...args);
this.writeToFile(logMessage);
this.originalConsole.log(...args);
}
error(...args) {
const logMessage = this.formatLogMessage('error', ...args);
this.writeToFile(logMessage);
this.originalConsole.error(...args);
}
warn(...args) {
const logMessage = this.formatLogMessage('warn', ...args);
this.writeToFile(logMessage);
this.originalConsole.warn(...args);
}
info(...args) {
const logMessage = this.formatLogMessage('info', ...args);
this.writeToFile(logMessage);
this.originalConsole.info(...args);
}
interceptConsole() {
console.log = (...args) => this.log(...args);
console.error = (...args) => this.error(...args);
console.warn = (...args) => this.warn(...args);
console.info = (...args) => this.info(...args);
process.on('uncaughtException', (error) => {
this.error('Uncaught exception:', error.stack || error.message);
});
process.on('unhandledRejection', (reason, promise) => {
this.error('Unhandled rejection at', promise, 'reason:', reason);
});
}
restoreConsole() {
console.log = this.originalConsole.log;
console.error = this.originalConsole.error;
console.warn = this.originalConsole.warn;
console.info = this.originalConsole.info;
}
getLogDirectory() {
return this.logDir;
}
updateInstallPath() {
this.initializeLogDirectory();
}
}
const logger = new Logger();
module.exports = logger;

View File

@@ -1,75 +0,0 @@
const fs = require('fs');
const path = require('path');
const AdmZip = require('adm-zip');
const { TOOLS_DIR } = require('../core/paths');
const { getOS, getArch } = require('../utils/platformUtils');
const { downloadFile } = require('../utils/fileManager');
async function installButler(toolsDir = TOOLS_DIR) {
if (!fs.existsSync(toolsDir)) {
fs.mkdirSync(toolsDir, { recursive: true });
}
const butlerName = process.platform === 'win32' ? 'butler.exe' : 'butler';
const butlerPath = path.join(toolsDir, butlerName);
const zipPath = path.join(toolsDir, 'butler.zip');
if (fs.existsSync(butlerPath)) {
return butlerPath;
}
let urls = [];
const osName = getOS();
const arch = getArch();
if (osName === 'windows') {
urls = ['https://broth.itch.zone/butler/windows-amd64/LATEST/archive/default'];
} else if (osName === 'darwin') {
if (arch === 'arm64') {
urls = [
'https://broth.itch.zone/butler/darwin-arm64/LATEST/archive/default',
'https://broth.itch.zone/butler/darwin-amd64/LATEST/archive/default'
];
} else {
urls = ['https://broth.itch.zone/butler/darwin-amd64/LATEST/archive/default'];
}
} else if (osName === 'linux') {
urls = ['https://broth.itch.zone/butler/linux-amd64/LATEST/archive/default'];
} else {
throw new Error('Operating system not supported');
}
console.log('Fetching Butler tool...');
let lastError = null;
for (const url of urls) {
try {
await downloadFile(url, zipPath);
lastError = null;
break;
} catch (error) {
lastError = error;
}
}
if (lastError) {
throw lastError;
}
console.log('Unpacking Butler...');
const zip = new AdmZip(zipPath);
zip.extractAllTo(toolsDir, true);
if (process.platform !== 'win32') {
fs.chmodSync(butlerPath, 0o755);
}
try {
fs.unlinkSync(zipPath);
} catch (err) {
console.log('Notice: could not delete butler.zip');
}
return butlerPath;
}
module.exports = {
installButler
};

View File

@@ -1,419 +0,0 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { exec } = require('child_process');
const { promisify } = require('util');
const { spawn } = require('child_process');
const { v4: uuidv4 } = require('uuid');
const { getResolvedAppDir, findClientPath } = require('../core/paths');
const { setupWaylandEnvironment } = require('../utils/platformUtils');
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain } = require('../core/config');
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager');
const { updateGameFiles } = require('./gameManager');
const { syncModsForCurrentProfile } = require('./modManager');
// Client patcher for custom auth server (sanasol.ws)
let clientPatcher = null;
try {
clientPatcher = require('../utils/clientPatcher');
} catch (err) {
console.log('[Launcher] Client patcher not available:', err.message);
}
const execAsync = promisify(exec);
// Fetch tokens from the auth server (properly signed with server's Ed25519 key)
async function fetchAuthTokens(uuid, name) {
const authServerUrl = getAuthServerUrl();
try {
console.log(`Fetching auth tokens from ${authServerUrl}/game-session/child`);
const response = await fetch(`${authServerUrl}/game-session/child`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
uuid: uuid,
name: name,
scopes: ['hytale:server', 'hytale:client']
})
});
if (!response.ok) {
throw new Error(`Auth server returned ${response.status}`);
}
const data = await response.json();
console.log('Auth tokens received from server');
return {
identityToken: data.IdentityToken || data.identityToken,
sessionToken: data.SessionToken || data.sessionToken
};
} catch (error) {
console.error('Failed to fetch auth tokens:', error.message);
// Fallback to local generation if server unavailable
return generateLocalTokens(uuid, name);
}
}
// Fallback: Generate tokens locally (won't pass signature validation but allows offline testing)
function generateLocalTokens(uuid, name) {
console.log('Using locally generated tokens (fallback mode)');
const authServerUrl = getAuthServerUrl();
const now = Math.floor(Date.now() / 1000);
const exp = now + 36000;
const header = Buffer.from(JSON.stringify({
alg: 'EdDSA',
kid: '2025-10-01',
typ: 'JWT'
})).toString('base64url');
const identityPayload = Buffer.from(JSON.stringify({
sub: uuid,
name: name,
username: name,
entitlements: ['game.base'],
scope: 'hytale:server hytale:client',
iat: now,
exp: exp,
iss: authServerUrl,
jti: uuidv4()
})).toString('base64url');
const sessionPayload = Buffer.from(JSON.stringify({
sub: uuid,
scope: 'hytale:server',
iat: now,
exp: exp,
iss: authServerUrl,
jti: uuidv4()
})).toString('base64url');
const signature = crypto.randomBytes(64).toString('base64url');
return {
identityToken: `${header}.${identityPayload}.${signature}`,
sessionToken: `${header}.${sessionPayload}.${signature}`
};
}
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
const customAppDir = getResolvedAppDir(installPathOverride);
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest');
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
const gameLatest = customGameDir;
let clientPath = findClientPath(gameLatest);
if (!clientPath) {
throw new Error('Game is not installed. Please install the game first.');
}
saveUsername(playerName);
if (installPathOverride) {
saveInstallPath(installPathOverride);
}
const configuredJava = (javaPathOverride !== undefined && javaPathOverride !== null
? javaPathOverride
: loadJavaPath() || '').trim();
let javaBin = null;
if (configuredJava) {
javaBin = await resolveJavaPath(configuredJava);
if (!javaBin) {
throw new Error(`Configured Java path not found: ${configuredJava}`);
}
} else {
javaBin = getJavaExec(customJreDir);
if (!getBundledJavaPath(customJreDir)) {
const fallback = await detectSystemJava();
if (fallback) {
javaBin = fallback;
} else {
throw new Error('Java runtime not found. Please install the game first or configure Java path.');
}
}
}
const uuid = getUuidForUser(playerName);
// Fetch tokens from auth server
if (progressCallback) {
progressCallback('Fetching authentication tokens...', null, null, null, null);
}
const { identityToken, sessionToken } = await fetchAuthTokens(uuid, playerName);
// Patch client and server binaries to use custom auth server (BEFORE signing on macOS)
const authDomain = getAuthDomain();
if (clientPatcher) {
try {
if (progressCallback) {
progressCallback('Patching game for custom server...', null, null, null, null);
}
console.log(`Patching game binaries for ${authDomain}...`);
const patchResult = await clientPatcher.ensureClientPatched(gameLatest, (msg, percent) => {
console.log(`[Patcher] ${msg}`);
if (progressCallback && msg) {
progressCallback(msg, percent, null, null, null);
}
});
if (patchResult.success) {
if (patchResult.alreadyPatched) {
console.log(`Game already patched for ${authDomain}`);
} else {
console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`);
if (patchResult.client) {
console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`);
}
if (patchResult.server) {
console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`);
}
}
} else {
console.warn('Game patching failed:', patchResult.error);
}
} catch (patchError) {
console.warn('Game patching failed (game may not connect to custom server):', patchError.message);
}
}
// macOS: Sign binaries AFTER patching so the patched binaries have valid signatures
if (process.platform === 'darwin') {
try {
const appBundle = path.join(gameLatest, 'Client', 'Hytale.app');
const serverDir = path.join(gameLatest, 'Server');
const signPath = async (targetPath, deep = false) => {
await execAsync(`xattr -cr "${targetPath}"`).catch(() => { });
const deepFlag = deep ? '--deep ' : '';
await execAsync(`codesign --force ${deepFlag}--sign - "${targetPath}"`).catch(() => { });
};
if (fs.existsSync(appBundle)) {
await signPath(appBundle, true);
console.log('Signed macOS app bundle (after patching)');
} else {
await signPath(path.dirname(clientPath), true);
console.log('Signed macOS client binary (after patching)');
}
if (javaBin && fs.existsSync(javaBin)) {
let jreRoot = path.dirname(path.dirname(javaBin));
if (jreRoot.endsWith('Home')) {
jreRoot = path.dirname(path.dirname(jreRoot));
}
await signPath(jreRoot, true);
await signPath(javaBin, false);
console.log('Signed Java runtime');
}
if (fs.existsSync(serverDir)) {
await execAsync(`xattr -cr "${serverDir}"`).catch(() => { });
await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => { });
console.log('Signed server binaries (after patching)');
}
if (javaBin && fs.existsSync(javaBin)) {
const javaWrapperPath = path.join(path.dirname(javaBin), 'java-wrapper');
const wrapperScript = `#!/bin/bash
# Java wrapper for macOS - adds --disable-sentry to fix Sentry hang issue
REAL_JAVA="${javaBin}"
ARGS=("$@")
for i in "\${!ARGS[@]}"; do
if [[ "\${ARGS[$i]}" == *"HytaleServer.jar"* ]]; then
ARGS=("\${ARGS[@]:0:$((i+1))}" "--disable-sentry" "\${ARGS[@]:$((i+1))}")
break
fi
done
exec "$REAL_JAVA" "\${ARGS[@]}"
`;
fs.writeFileSync(javaWrapperPath, wrapperScript, { mode: 0o755 });
await signPath(javaWrapperPath, false);
console.log('Created java wrapper with --disable-sentry fix');
javaBin = javaWrapperPath;
}
} catch (signError) {
console.log('Notice: macOS signing step failed:', signError.message);
console.log('The game may still launch if Gatekeeper allows it');
}
}
const args = [
'--app-dir', gameLatest,
'--java-exec', javaBin,
'--auth-mode', 'authenticated',
'--uuid', uuid,
'--name', playerName,
'--identity-token', identityToken,
'--session-token', sessionToken,
'--user-dir', userDataDir
];
if (progressCallback) {
progressCallback('Starting game...', null, null, null, null);
}
// Ensure mods are synced for the active profile before launching
try {
console.log('Syncing mods for active profile before launch...');
if (progressCallback) progressCallback('Syncing mods...', null, null, null, null);
await syncModsForCurrentProfile();
} catch (syncError) {
console.error('Failed to sync mods before launch:', syncError);
// Continue anyway? Or fail?
// Warn user but continue might be safer to avoid blocking play if sync is just glitchy
}
console.log('Starting game...');
console.log(`Command: "${clientPath}" ${args.join(' ')}`);
const env = { ...process.env };
const waylandEnv = setupWaylandEnvironment();
Object.assign(env, waylandEnv);
try {
let spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'],
detached: true,
env: env
};
if (process.platform === 'win32') {
spawnOptions.shell = false;
spawnOptions.windowsHide = true;
}
const child = spawn(clientPath, args, spawnOptions);
console.log(`Game process started with PID: ${child.pid}`);
let hasExited = false;
let outputReceived = false;
child.stdout.on('data', (data) => {
outputReceived = true;
console.log(`Game output: ${data.toString().trim()}`);
});
child.stderr.on('data', (data) => {
outputReceived = true;
console.error(`Game error: ${data.toString().trim()}`);
});
child.on('error', (error) => {
hasExited = true;
console.error(`Failed to start game process: ${error.message}`);
if (progressCallback) {
progressCallback(`Failed to start game: ${error.message}`, -1, null, null, null);
}
});
child.on('exit', (code, signal) => {
hasExited = true;
if (code !== null) {
console.log(`Game process exited with code ${code}`);
if (code !== 0 && progressCallback) {
progressCallback(`Game exited with error code ${code}`, -1, null, null, null);
}
} else if (signal) {
console.log(`Game process terminated by signal ${signal}`);
}
});
setTimeout(() => {
if (!hasExited) {
console.log('Game appears to be running successfully');
child.unref();
if (progressCallback) {
progressCallback('Game launched successfully', 100, null, null, null);
}
} else if (!outputReceived) {
console.warn('Game process exited immediately with no output - possible issue with game files or dependencies');
}
}, 3000);
return { success: true, installed: true, launched: true, pid: child.pid };
} catch (spawnError) {
console.error(`Error spawning game process: ${spawnError.message}`);
if (progressCallback) {
progressCallback(`Error launching game: ${spawnError.message}`, -1, null, null, null);
}
throw spawnError;
}
}
async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
try {
if (progressCallback) {
progressCallback('Checking for updates...', 0, null, null, null);
}
const [installedVersion, latestVersion] = await Promise.all([
getInstalledClientVersion(),
getLatestClientVersion()
]);
console.log(`Installed version: ${installedVersion}, Latest version: ${latestVersion}`);
let needsUpdate = false;
if (installedVersion && latestVersion && installedVersion !== latestVersion) {
needsUpdate = true;
console.log('Version mismatch detected, update required');
}
if (needsUpdate) {
if (progressCallback) {
progressCallback('Game update required, starting update process...', 10, null, null, null);
}
const customAppDir = getResolvedAppDir(installPathOverride);
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
const customToolsDir = path.join(customAppDir, 'butler');
const customCacheDir = path.join(customAppDir, 'cache');
try {
await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir);
console.log('Game updated successfully, waiting before launch...');
if (progressCallback) {
progressCallback('Preparing game launch...', 90, null, null, null);
}
await new Promise(resolve => setTimeout(resolve, 3000));
} catch (updateError) {
console.error('Update failed:', updateError);
if (progressCallback) {
progressCallback(`Update failed: ${updateError.message}`, -1, null, null, null);
}
throw updateError;
}
}
if (progressCallback) {
progressCallback('Launching game...', 80, null, null, null);
}
return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride);
} catch (error) {
console.error('Error in version check and launch:', error);
if (progressCallback) {
progressCallback(`Error: ${error.message}`, -1, null, null, null);
}
throw error;
}
}
module.exports = {
launchGame,
launchGameWithVersionCheck
};

View File

@@ -1,398 +0,0 @@
const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursive, GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
const { getOS, getArch } = require('../utils/platformUtils');
const { downloadFile } = require('../utils/fileManager');
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
const { installButler } = require('./butlerManager');
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig } = require('../core/config');
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
async function downloadPWR(version = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR) {
const osName = getOS();
const arch = getArch();
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${version}/0/${fileName}`;
const dest = path.join(cacheDir, fileName);
if (fs.existsSync(dest)) {
console.log('PWR file found in cache:', dest);
return dest;
}
console.log('Fetching PWR patch file:', url);
await downloadFile(url, dest, progressCallback);
console.log('PWR saved to:', dest);
return dest;
}
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR) {
const butlerPath = await installButler(toolsDir);
const gameLatest = gameDir;
const stagingDir = path.join(gameLatest, 'staging-temp');
const clientPath = findClientPath(gameLatest);
if (clientPath) {
console.log('Game files detected, skipping patch installation.');
return;
}
if (!fs.existsSync(gameLatest)) {
fs.mkdirSync(gameLatest, { recursive: true });
}
if (!fs.existsSync(stagingDir)) {
fs.mkdirSync(stagingDir, { recursive: true });
}
if (progressCallback) {
progressCallback('Installing game patch...', null, null, null, null);
}
console.log('Installing game patch...');
if (!fs.existsSync(butlerPath)) {
throw new Error(`Butler tool not found at: ${butlerPath}`);
}
if (!fs.existsSync(pwrFile)) {
throw new Error(`PWR file not found at: ${pwrFile}`);
}
const args = [
'apply',
'--staging-dir',
stagingDir,
pwrFile,
gameLatest
];
try {
await new Promise((resolve, reject) => {
const child = execFile(butlerPath, args, {
maxBuffer: 1024 * 1024 * 10,
timeout: 600000
}, (error, stdout, stderr) => {
if (error) {
console.error('Butler stderr:', stderr);
console.error('Butler stdout:', stdout);
reject(new Error(`Patch installation failed: ${error.message}${stderr ? '\n' + stderr : ''}`));
} else {
resolve();
}
});
});
} catch (error) {
throw error;
}
if (fs.existsSync(stagingDir)) {
fs.rmSync(stagingDir, { recursive: true, force: true });
}
if (progressCallback) {
progressCallback('Installation complete', null, null, null, null);
}
console.log('Installation complete');
}
async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, cacheDir = CACHE_DIR) {
let tempUpdateDir;
try {
if (progressCallback) {
progressCallback('Updating game files...', 0, null, null, null);
}
console.log(`Updating game files to version: ${newVersion}`);
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
if (fs.existsSync(tempUpdateDir)) {
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
}
fs.mkdirSync(tempUpdateDir, { recursive: true });
if (progressCallback) {
progressCallback('Downloading new game version...', 10, null, null, null);
}
const pwrFile = await downloadPWR('release', newVersion, progressCallback, cacheDir);
if (progressCallback) {
progressCallback('Extracting new files...', 50, null, null, null);
}
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir);
if (progressCallback) {
progressCallback('Replacing game files...', 80, null, null, null);
}
let userDataBackup = null;
const userDataPath = findUserDataRecursive(gameDir);
if (userDataPath && fs.existsSync(userDataPath)) {
userDataBackup = path.join(gameDir, '..', 'UserData_backup_' + Date.now());
console.log(`Backing up UserData from ${userDataPath} to: ${userDataBackup}`);
function copyRecursive(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const files = fs.readdirSync(src);
for (const file of files) {
copyRecursive(path.join(src, file), path.join(dest, file));
}
} else {
fs.copyFileSync(src, dest);
}
}
copyRecursive(userDataPath, userDataBackup);
} else {
console.log('No UserData folder found in game directory');
}
if (fs.existsSync(gameDir)) {
console.log('Removing old game files...');
fs.rmSync(gameDir, { recursive: true, force: true });
}
fs.renameSync(tempUpdateDir, gameDir);
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
console.log('HomePage.ui update result after update:', homeUIResult);
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
console.log('Logo@2x.png update result after update:', logoResult);
if (userDataBackup && fs.existsSync(userDataBackup)) {
const newUserDataPath = findUserDataPath(gameDir);
const userDataParent = path.dirname(newUserDataPath);
if (!fs.existsSync(userDataParent)) {
fs.mkdirSync(userDataParent, { recursive: true });
}
console.log(`Restoring UserData to: ${newUserDataPath}`);
function copyRecursive(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const files = fs.readdirSync(src);
for (const file of files) {
copyRecursive(path.join(src, file), path.join(dest, file));
}
} else {
fs.copyFileSync(src, dest);
}
}
copyRecursive(userDataBackup, newUserDataPath);
}
console.log(`Game files updated successfully to version: ${newVersion}`);
if (userDataBackup && fs.existsSync(userDataBackup)) {
try {
fs.rmSync(userDataBackup, { recursive: true, force: true });
console.log('UserData backup cleaned up');
} catch (cleanupError) {
console.warn('Could not clean up UserData backup:', cleanupError.message);
}
}
console.log('Waiting for file system sync...');
await new Promise(resolve => setTimeout(resolve, 2000));
if (progressCallback) {
progressCallback('Game update completed', 100, null, null, null);
}
return { success: true, updated: true, version: newVersion };
} catch (error) {
console.error('Error updating game files:', error);
if (userDataBackup && fs.existsSync(userDataBackup)) {
try {
fs.rmSync(userDataBackup, { recursive: true, force: true });
console.log('UserData backup cleaned up after error');
} catch (cleanupError) {
console.warn('Could not clean up UserData backup:', cleanupError.message);
}
}
if (tempUpdateDir && fs.existsSync(tempUpdateDir)) {
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
}
throw new Error(`Failed to update game files: ${error.message}`);
}
}
function isGameInstalled() {
const appDir = getResolvedAppDir();
const gameDir = path.join(appDir, 'release', 'package', 'game', 'latest');
const clientPath = findClientPath(gameDir);
return clientPath !== null;
}
async function installGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
const customAppDir = getResolvedAppDir(installPathOverride);
const customCacheDir = path.join(customAppDir, 'cache');
const customToolsDir = path.join(customAppDir, 'butler');
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest');
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
[customAppDir, customCacheDir, customToolsDir].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
if (!fs.existsSync(userDataDir)) {
fs.mkdirSync(userDataDir, { recursive: true });
}
saveUsername(playerName);
if (installPathOverride) {
saveInstallPath(installPathOverride);
}
const gameLatest = customGameDir;
let clientPath = findClientPath(gameLatest);
if (clientPath) {
if (progressCallback) {
progressCallback('Game already installed', 100, null, null, null);
}
console.log('Game is already installed');
return { success: true, alreadyInstalled: true };
}
const configuredJava = (javaPathOverride !== undefined && javaPathOverride !== null
? javaPathOverride
: loadJavaPath() || '').trim();
let javaBin = null;
if (configuredJava) {
javaBin = await resolveJavaPath(configuredJava);
if (!javaBin) {
throw new Error(`Configured Java path not found: ${configuredJava}`);
}
} else {
try {
await downloadJRE(progressCallback, customCacheDir, customJreDir);
} catch (error) {
const fallback = await detectSystemJava();
if (fallback) {
javaBin = fallback;
} else {
throw error;
}
}
if (!javaBin) {
javaBin = getJavaExec(customJreDir);
}
}
if (progressCallback) {
progressCallback('Fetching game files...', null, null, null, null);
}
console.log('Installing game files...');
const latestVersion = await getLatestClientVersion();
const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir);
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir);
const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
console.log('HomePage.ui update result after installation:', homeUIResult);
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
console.log('Logo@2x.png update result after installation:', logoResult);
if (progressCallback) {
progressCallback('Installation complete', 100, null, null, null);
}
console.log('Game installation completed successfully');
return {
success: true,
installed: true
};
}
async function uninstallGame() {
const appDir = getResolvedAppDir();
if (!fs.existsSync(appDir)) {
throw new Error('Game is not installed');
}
try {
fs.rmSync(appDir, { recursive: true, force: true });
console.log('Game uninstalled successfully - removed entire HytaleF2P folder');
if (fs.existsSync(CONFIG_FILE)) {
const config = loadConfig();
delete config.installPath;
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
}
} catch (error) {
throw new Error(`Failed to uninstall game: ${error.message}`);
}
}
function checkExistingGameInstallation() {
try {
const config = loadConfig();
if (!config.installPath || !config.installPath.trim()) {
return null;
}
const installPath = config.installPath.trim();
const gameDir = path.join(installPath, 'HytaleF2P', 'release', 'package', 'game', 'latest');
if (!fs.existsSync(gameDir)) {
return null;
}
const clientPath = findClientPath(gameDir);
if (!clientPath) {
return null;
}
const userDataPath = findUserDataRecursive(gameDir);
return {
gameDir: gameDir,
clientPath: clientPath,
userDataPath: userDataPath,
installPath: installPath,
hasUserData: userDataPath && fs.existsSync(userDataPath)
};
} catch (error) {
console.error('Error checking existing game installation:', error);
return null;
}
}
module.exports = {
downloadPWR,
applyPWR,
updateGameFiles,
isGameInstalled,
installGame,
uninstallGame,
checkExistingGameInstallation
};

View File

@@ -1,363 +0,0 @@
const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
const { promisify } = require('util');
const axios = require('axios');
const AdmZip = require('adm-zip');
const crypto = require('crypto');
const tar = require('tar');
const { expandHome, JRE_DIR } = require('../core/paths');
const { getOS, getArch } = require('../utils/platformUtils');
const { loadConfig } = require('../core/config');
const { downloadFile } = require('../utils/fileManager');
const execFileAsync = promisify(execFile);
const JAVA_EXECUTABLE = 'java' + (process.platform === 'win32' ? '.exe' : '');
async function findJavaOnPath(commandName = 'java') {
const lookupCmd = process.platform === 'win32' ? 'where' : 'which';
try {
const { stdout } = await execFileAsync(lookupCmd, [commandName]);
const line = stdout.split(/\r?\n/).map(lineItem => lineItem.trim()).find(Boolean);
return line || null;
} catch (err) {
return null;
}
}
async function getMacJavaHome() {
if (process.platform !== 'darwin') {
return null;
}
try {
const { stdout } = await execFileAsync('/usr/libexec/java_home');
const home = stdout.trim();
if (!home) {
return null;
}
return path.join(home, 'bin', JAVA_EXECUTABLE);
} catch (err) {
return null;
}
}
async function resolveJavaPath(inputPath) {
const trimmed = (inputPath || '').trim();
if (!trimmed) {
return null;
}
const expanded = expandHome(trimmed);
if (fs.existsSync(expanded)) {
const stat = fs.statSync(expanded);
if (stat.isDirectory()) {
const candidate = path.join(expanded, 'bin', JAVA_EXECUTABLE);
return fs.existsSync(candidate) ? candidate : null;
}
return expanded;
}
if (!path.isAbsolute(expanded)) {
return await findJavaOnPath(trimmed);
}
return null;
}
async function detectSystemJava() {
const envHome = process.env.JAVA_HOME;
if (envHome) {
const envJava = path.join(envHome, 'bin', JAVA_EXECUTABLE);
if (fs.existsSync(envJava)) {
return envJava;
}
}
const macJava = await getMacJavaHome();
if (macJava && fs.existsSync(macJava)) {
return macJava;
}
const pathJava = await findJavaOnPath('java');
if (pathJava && fs.existsSync(pathJava)) {
return pathJava;
}
return null;
}
function loadJavaPath() {
const config = loadConfig();
return config.javaPath || '';
}
function getBundledJavaPath(jreDir = JRE_DIR) {
const candidates = [
path.join(jreDir, 'bin', JAVA_EXECUTABLE)
];
if (process.platform === 'darwin') {
candidates.push(path.join(jreDir, 'Contents', 'Home', 'bin', JAVA_EXECUTABLE));
}
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
function getJavaExec(jreDir = JRE_DIR) {
const bundledJava = getBundledJavaPath(jreDir);
if (bundledJava) {
return bundledJava;
}
console.log('Notice: Java runtime not found, using system default');
return 'java';
}
async function getJavaDetection() {
const candidates = [];
const bundledJava = getBundledJavaPath() || path.join(JRE_DIR, 'bin', JAVA_EXECUTABLE);
candidates.push({
label: 'Bundled JRE',
path: bundledJava,
exists: fs.existsSync(bundledJava)
});
const javaHomeEnv = process.env.JAVA_HOME;
if (javaHomeEnv) {
const envJava = path.join(javaHomeEnv, 'bin', JAVA_EXECUTABLE);
candidates.push({
label: 'JAVA_HOME',
path: envJava,
exists: fs.existsSync(envJava),
note: fs.existsSync(envJava) ? '' : 'Not found'
});
} else {
candidates.push({
label: 'JAVA_HOME',
path: '',
exists: false,
note: 'Not set'
});
}
if (process.platform === 'darwin') {
const macJava = await getMacJavaHome();
if (macJava) {
candidates.push({
label: 'java_home',
path: macJava,
exists: fs.existsSync(macJava),
note: fs.existsSync(macJava) ? '' : 'Not found'
});
} else {
candidates.push({
label: 'java_home',
path: '',
exists: false,
note: 'Not found'
});
}
}
const pathJava = await findJavaOnPath('java');
if (pathJava) {
candidates.push({
label: 'PATH',
path: pathJava,
exists: true
});
} else {
candidates.push({
label: 'PATH',
path: '',
exists: false,
note: 'java not found'
});
}
return {
javaPath: loadJavaPath(),
candidates
};
}
async function downloadJRE(progressCallback, cacheDir, jreDir = JRE_DIR) {
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
const osName = getOS();
const arch = getArch();
const bundledJava = getBundledJavaPath(jreDir);
if (bundledJava) {
console.log('Java runtime found, skipping download');
return;
}
console.log('Requesting Java runtime information...');
const response = await axios.get('https://launcher.hytale.com/version/release/jre.json', {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'application/json',
'Accept-Language': 'en-US,en;q=0.9'
}
});
const jreData = response.data;
const osData = jreData.download_url[osName];
if (!osData) {
throw new Error(`Java runtime unavailable for platform: ${osName}`);
}
const platform = osData[arch];
if (!platform) {
throw new Error(`Java runtime unavailable for architecture ${arch} on ${osName}`);
}
const fileName = path.basename(platform.url);
const cacheFile = path.join(cacheDir, fileName);
if (!fs.existsSync(cacheFile)) {
if (progressCallback) {
progressCallback('Fetching Java runtime...', null, null, null, null);
}
console.log('Fetching Java runtime...');
await downloadFile(platform.url, cacheFile, progressCallback);
console.log('Download finished');
}
if (progressCallback) {
progressCallback('Validating files...', null, null, null, null);
}
console.log('Validating files...');
const fileBuffer = fs.readFileSync(cacheFile);
const hashSum = crypto.createHash('sha256');
hashSum.update(fileBuffer);
const hex = hashSum.digest('hex');
if (hex !== platform.sha256) {
fs.unlinkSync(cacheFile);
throw new Error(`File validation failed: expected ${platform.sha256} but got ${hex}`);
}
if (progressCallback) {
progressCallback('Unpacking Java runtime...', null, null, null, null);
}
console.log('Unpacking Java runtime...');
await extractJRE(cacheFile, jreDir);
if (process.platform !== 'win32') {
const javaCandidates = [
path.join(jreDir, 'bin', JAVA_EXECUTABLE),
path.join(jreDir, 'Contents', 'Home', 'bin', JAVA_EXECUTABLE)
];
for (const javaPath of javaCandidates) {
if (fs.existsSync(javaPath)) {
fs.chmodSync(javaPath, 0o755);
}
}
}
flattenJREDir(jreDir);
try {
fs.unlinkSync(cacheFile);
} catch (err) {
console.log('Notice: could not delete cached Java files:', err.message);
}
console.log('Java runtime ready');
}
async function extractJRE(archivePath, destDir) {
if (fs.existsSync(destDir)) {
fs.rmSync(destDir, { recursive: true, force: true });
}
fs.mkdirSync(destDir, { recursive: true });
if (archivePath.endsWith('.zip')) {
return extractZip(archivePath, destDir);
} else if (archivePath.endsWith('.tar.gz')) {
return extractTarGz(archivePath, destDir);
} else {
throw new Error(`Archive type not supported: ${archivePath}`);
}
}
function extractZip(zipPath, dest) {
const zip = new AdmZip(zipPath);
const entries = zip.getEntries();
for (const entry of entries) {
const entryPath = path.join(dest, entry.entryName);
const resolvedPath = path.resolve(entryPath);
const resolvedDest = path.resolve(dest);
if (!resolvedPath.startsWith(resolvedDest)) {
throw new Error(`Invalid file path detected: ${entryPath}`);
}
if (entry.isDirectory) {
fs.mkdirSync(entryPath, { recursive: true });
} else {
fs.mkdirSync(path.dirname(entryPath), { recursive: true });
fs.writeFileSync(entryPath, entry.getData());
if (process.platform !== 'win32') {
fs.chmodSync(entryPath, entry.header.attr >>> 16);
}
}
}
}
function extractTarGz(tarGzPath, dest) {
return tar.extract({
file: tarGzPath,
cwd: dest,
strip: 0
});
}
function flattenJREDir(jreLatest) {
try {
const entries = fs.readdirSync(jreLatest, { withFileTypes: true });
if (entries.length !== 1 || !entries[0].isDirectory()) {
return;
}
const nested = path.join(jreLatest, entries[0].name);
const files = fs.readdirSync(nested, { withFileTypes: true });
for (const file of files) {
const oldPath = path.join(nested, file.name);
const newPath = path.join(jreLatest, file.name);
fs.renameSync(oldPath, newPath);
}
fs.rmSync(nested, { recursive: true, force: true });
} catch (err) {
console.log('Notice: could not restructure Java directory:', err.message);
}
}
module.exports = {
findJavaOnPath,
getMacJavaHome,
resolveJavaPath,
detectSystemJava,
loadJavaPath,
getBundledJavaPath,
getJavaExec,
getJavaDetection,
downloadJRE,
extractJRE,
JAVA_EXECUTABLE
};

View File

@@ -1,334 +0,0 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const axios = require('axios');
const { getModsPath } = require('../core/paths');
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
const profileManager = require('./profileManager');
function generateModId(filename) {
return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8);
}
function extractModName(filename) {
let name = path.parse(filename).name;
name = name.replace(/-v?\d+\.[\d\.]+.*$/i, '');
name = name.replace(/-\d+\.[\d\.]+.*$/i, '');
name = name.replace(/[-_]/g, ' ');
name = name.replace(/\b\w/g, l => l.toUpperCase());
return name || 'Unknown Mod';
}
function extractVersion(filename) {
const versionMatch = filename.match(/v?(\d+\.[\d\.]+)/);
return versionMatch ? versionMatch[1] : null;
}
// Helper to get mods from active profile
function getProfileMods() {
const profile = profileManager.getActiveProfile();
return profile ? (profile.mods || []) : [];
}
async function loadInstalledMods(modsPath) {
try {
const activeProfile = profileManager.getActiveProfile();
if (!activeProfile) return [];
const profileMods = activeProfile.mods || [];
const profileModFiles = new Set(profileMods.map(m => m.fileName));
// We only return mods that are explicitly in the profile
// Check which ones are physically present (either in mods/ or DisabledMods/)
const physicalModsPath = modsPath; // .../mods
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
const validMods = [];
for (const modConfig of profileMods) {
// Check if file exists in either location
const inEnabled = fs.existsSync(path.join(physicalModsPath, modConfig.fileName));
const inDisabled = fs.existsSync(path.join(disabledModsPath, modConfig.fileName));
if (inEnabled || inDisabled) {
validMods.push({
...modConfig,
// Set filePath based on physical location
filePath: inEnabled ? path.join(physicalModsPath, modConfig.fileName) : path.join(disabledModsPath, modConfig.fileName),
enabled: modConfig.enabled !== false // Default true
});
} else {
console.warn(`[ModManager] Mod ${modConfig.fileName} listed in profile but not found on disk.`);
// Include it so user can see it's missing or remove it
validMods.push({
...modConfig,
filePath: null,
missing: true,
enabled: modConfig.enabled !== false
});
}
}
return validMods;
} catch (error) {
console.error('Error loading installed mods:', error);
return [];
}
}
async function downloadMod(modInfo) {
try {
const modsPath = await getModsPath();
if (!modInfo.downloadUrl && !modInfo.fileId) {
throw new Error('No download URL or file ID provided');
}
let downloadUrl = modInfo.downloadUrl;
if (!downloadUrl && modInfo.fileId && modInfo.modId) {
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId}/files/${modInfo.fileId}`, {
headers: {
'x-api-key': modInfo.apiKey,
'Accept': 'application/json'
}
});
downloadUrl = response.data.data.downloadUrl;
}
if (!downloadUrl) {
throw new Error('Could not determine download URL');
}
const fileName = modInfo.fileName || `mod-${modInfo.modId}.jar`;
const filePath = path.join(modsPath, fileName);
const response = await axios({
method: 'get',
url: downloadUrl,
responseType: 'stream'
});
const writer = fs.createWriteStream(filePath);
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on('finish', () => {
// NEW: Update Active Profile instead of global config
const activeProfile = profileManager.getActiveProfile();
if (activeProfile) {
const newMod = {
id: modInfo.id || generateModId(fileName),
name: modInfo.name || extractModName(fileName),
version: modInfo.version || '1.0.0',
description: modInfo.summary || modInfo.description || 'Downloaded from CurseForge',
author: modInfo.author || 'Unknown',
enabled: true,
fileName: fileName,
fileSize: fs.statSync(filePath).size,
dateInstalled: new Date().toISOString(),
curseForgeId: modInfo.modId,
curseForgeFileId: modInfo.fileId
};
const updatedMods = [...(activeProfile.mods || []), newMod];
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
resolve({
success: true,
filePath: filePath,
fileName: fileName,
modInfo: newMod
});
} else {
reject(new Error('No active profile to save mod to'));
}
});
writer.on('error', reject);
});
} catch (error) {
console.error('Error downloading mod:', error);
return {
success: false,
error: error.message
};
}
}
async function uninstallMod(modId, modsPath) {
try {
const activeProfile = profileManager.getActiveProfile();
if (!activeProfile) throw new Error('No active profile');
const profileMods = activeProfile.mods || [];
const mod = profileMods.find(m => m.id === modId);
if (!mod) {
throw new Error('Mod not found in profile');
}
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
const enabledPath = path.join(modsPath, mod.fileName);
const disabledPath = path.join(disabledModsPath, mod.fileName);
let fileRemoved = false;
// Try to remove file from both locations to be safe
if (fs.existsSync(enabledPath)) {
fs.unlinkSync(enabledPath);
fileRemoved = true;
}
if (fs.existsSync(disabledPath)) {
try { fs.unlinkSync(disabledPath); fileRemoved = true; } catch (e) { }
}
if (!fileRemoved) {
console.warn('Mod file not found on filesystem, removing from profile anyway');
}
const updatedMods = profileMods.filter(m => m.id !== modId);
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
console.log('Mod removed from profile');
return { success: true };
} catch (error) {
console.error('Error uninstalling mod:', error);
return {
success: false,
error: error.message
};
}
}
async function toggleMod(modId, modsPath) {
try {
const activeProfile = profileManager.getActiveProfile();
if (!activeProfile) throw new Error('No active profile');
const profileMods = activeProfile.mods || [];
const modIndex = profileMods.findIndex(m => m.id === modId);
if (modIndex === -1) {
throw new Error('Mod not found in profile');
}
const mod = profileMods[modIndex];
const newEnabled = !mod.enabled; // Toggle
// Update Profile First
const updatedMods = [...profileMods];
updatedMods[modIndex] = { ...mod, enabled: newEnabled };
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
// Manually move the file to reflect the new state
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
if (!fs.existsSync(disabledModsPath)) fs.mkdirSync(disabledModsPath, { recursive: true });
const currentPath = mod.enabled ? path.join(modsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName);
// Determine target paths
const targetDir = newEnabled ? modsPath : disabledModsPath;
const targetPath = path.join(targetDir, mod.fileName);
if (fs.existsSync(currentPath)) {
fs.renameSync(currentPath, targetPath);
} else {
// Fallback: check if it's already in target?
if (fs.existsSync(targetPath)) {
// It's already there, maybe just state was wrong.
console.log(`[ModManager] Mod ${mod.fileName} is already in the correct state`);
} else {
// Try finding it
const altPath = mod.enabled ? path.join(disabledModsPath, mod.fileName) : path.join(modsPath, mod.fileName);
if (fs.existsSync(altPath)) fs.renameSync(altPath, targetPath);
}
}
return { success: true, enabled: newEnabled };
} catch (error) {
console.error('Error toggling mod:', error);
return {
success: false,
error: error.message
};
}
}
async function syncModsForCurrentProfile() {
try {
const activeProfile = profileManager.getActiveProfile();
if (!activeProfile) {
console.warn('No active profile found during mod sync');
return;
}
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name}`);
const modsPath = await getModsPath();
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
if (!fs.existsSync(disabledModsPath)) {
fs.mkdirSync(disabledModsPath, { recursive: true });
}
// Get all physical files from both folders
const enabledFiles = fs.existsSync(modsPath) ? fs.readdirSync(modsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
const disabledFiles = fs.existsSync(disabledModsPath) ? fs.readdirSync(disabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
const allFiles = new Set([...enabledFiles, ...disabledFiles]);
// Profile.mods contains the list of ALL mods for that profile, with their enabled state.
const profileMods = activeProfile.mods || [];
for (const fileName of allFiles) {
const modConfig = profileMods.find(m => m.fileName === fileName);
const shouldBeEnabled = modConfig && modConfig.enabled !== false; // Default to true if in list, unless explicitly false
// Logic:
// If it should be enabled -> Move to mods/
// If it should be disabled -> Move to DisabledMods/
const currentPath = enabledFiles.includes(fileName) ? path.join(modsPath, fileName) : path.join(disabledModsPath, fileName);
const targetDir = shouldBeEnabled ? modsPath : disabledModsPath;
const targetPath = path.join(targetDir, fileName);
if (path.dirname(currentPath) !== targetDir) {
console.log(`[Mod Sync] Moving ${fileName} to ${shouldBeEnabled ? 'Enabled' : 'Disabled'}`);
try {
fs.renameSync(currentPath, targetPath);
} catch (err) {
console.error(`Failed to move ${fileName}: ${err.message}`);
}
}
}
return { success: true };
} catch (error) {
console.error('[ModManager] Error syncing mods:', error);
return { success: false, error: error.message };
}
}
module.exports = {
loadInstalledMods,
downloadMod,
uninstallMod,
toggleMod,
syncModsForCurrentProfile,
generateModId,
extractModName,
extractVersion
};

View File

@@ -1,206 +0,0 @@
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const {
loadConfig,
saveConfig,
getModsPath
} = require('../core/config');
// Lazy-load modManager to avoid circular deps, or keep imports structured.
// For now, access mod paths directly or use helper helpers.
class ProfileManager {
constructor() {
this.initialized = false;
}
init() {
if (this.initialized) return;
const config = loadConfig();
// Migration: specific check to see if we have profiles yet
if (!config.profiles || Object.keys(config.profiles).length === 0) {
this.migrateLegacyConfig(config);
}
this.initialized = true;
console.log('[ProfileManager] Initialized');
}
migrateLegacyConfig(config) {
console.log('[ProfileManager] Migrating legacy config to profile system...');
// Create a default profile with current settings
const defaultProfileId = 'default';
const defaultProfile = {
id: defaultProfileId,
name: 'Default',
created: new Date().toISOString(),
lastUsed: new Date().toISOString(),
// settings specific to this profile
// If global settings existed, we copy them here
mods: config.installedMods || [], // Legacy mods are now part of default profile
javaPath: config.javaPath || '',
gameOptions: {
minMemory: '1G',
maxMemory: '4G',
args: []
}
};
const updates = {
profiles: {
[defaultProfileId]: defaultProfile
},
activeProfileId: defaultProfileId,
// Mods are currently treated as files on disk.
// The profile's `mods` array is the source of truth for enabled/known mods per profile.
};
saveConfig(updates);
console.log('[ProfileManager] Migration complete. Created Default profile.');
}
createProfile(name) {
if (!name || typeof name !== 'string') {
throw new Error('Invalid profile name');
}
const config = loadConfig();
const id = uuidv4();
const newProfile = {
id,
name: name.trim(),
created: new Date().toISOString(),
lastUsed: null,
mods: [], // Start with no mods enabled
javaPath: '',
gameOptions: {
minMemory: '1G',
maxMemory: '4G',
args: []
}
};
const profiles = config.profiles || {};
profiles[id] = newProfile;
saveConfig({ profiles });
console.log(`[ProfileManager] Created new profile: "${name}" (${id})`);
return newProfile;
}
getProfiles() {
const config = loadConfig();
return Object.values(config.profiles || {});
}
getProfile(id) {
const config = loadConfig();
return (config.profiles && config.profiles[id]) || null;
}
getActiveProfile() {
const config = loadConfig();
const activeId = config.activeProfileId;
if (!activeId || !config.profiles || !config.profiles[activeId]) {
// Fallback if something is corrupted
return this.getProfiles()[0] || null;
}
return config.profiles[activeId];
}
async activateProfile(id) {
const config = loadConfig();
if (!config.profiles || !config.profiles[id]) {
throw new Error(`Profile not found: ${id}`);
}
if (config.activeProfileId === id) {
console.log(`[ProfileManager] Profile ${id} is already active.`);
return config.profiles[id];
}
console.log(`[ProfileManager] Switching to profile: ${config.profiles[id].name} (${id})`);
// 1. Update config first
config.profiles[id].lastUsed = new Date().toISOString();
saveConfig({
activeProfileId: id,
profiles: config.profiles
});
// 2. Trigger Mod Sync
// We need to require this here to ensure it uses the *newly saved* active profile ID
const { syncModsForCurrentProfile } = require('./modManager');
await syncModsForCurrentProfile();
return config.profiles[id];
}
deleteProfile(id) {
const config = loadConfig();
const profiles = config.profiles || {};
if (!profiles[id]) {
throw new Error('Profile not found');
}
if (config.activeProfileId === id) {
throw new Error('Cannot delete the active profile');
}
// Don't allow deleting the last profile
if (Object.keys(profiles).length <= 1) {
throw new Error('Cannot delete the only remaining profile');
}
delete profiles[id];
saveConfig({ profiles });
console.log(`[ProfileManager] Deleted profile: ${id}`);
return true;
}
updateProfile(id, updates) {
const config = loadConfig();
const profiles = config.profiles || {};
if (!profiles[id]) {
throw new Error('Profile not found');
}
// Safety checks on updates
const allowedFields = ['name', 'javaPath', 'gameOptions', 'mods'];
const sanitizedUpdates = {};
Object.keys(updates).forEach(key => {
if (allowedFields.includes(key)) {
sanitizedUpdates[key] = updates[key];
}
});
profiles[id] = { ...profiles[id], ...sanitizedUpdates };
saveConfig({ profiles });
console.log(`[ProfileManager] Updated profile: ${id}`);
// If we updated mods for the *active* profile, we might need to sync immediately
if (config.activeProfileId === id && updates.mods) {
// Optionally trigger sync?
// Sync is usually triggered when toggling a single mod.
// For bulk updates, the caller should decide when to sync.
}
return profiles[id];
}
}
module.exports = new ProfileManager();

View File

@@ -1,116 +0,0 @@
const fs = require('fs');
const path = require('path');
const { downloadFile, findHomePageUIPath, findLogoPath } = require('../utils/fileManager');
async function downloadAndReplaceHomePageUI(gameDir, progressCallback) {
try {
console.log('Downloading HomePage.ui from server...');
if (progressCallback) {
progressCallback('Downloading HomePage.ui...', null, null, null, null);
}
const homeUIUrl = 'https://files.hytalef2p.com/api/HomeUI';
const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui');
await downloadFile(homeUIUrl, tempHomePath);
const existingHomePath = findHomePageUIPath(gameDir);
if (existingHomePath && fs.existsSync(existingHomePath)) {
console.log('Found existing HomePage.ui at:', existingHomePath);
const backupPath = existingHomePath + '.backup';
if (!fs.existsSync(backupPath)) {
fs.copyFileSync(existingHomePath, backupPath);
console.log('Original HomePage.ui backed up');
}
fs.copyFileSync(tempHomePath, existingHomePath);
console.log('HomePage.ui replaced successfully');
} else {
console.log('No existing HomePage.ui found, skipping replacement');
}
if (fs.existsSync(tempHomePath)) {
fs.unlinkSync(tempHomePath);
}
if (progressCallback) {
progressCallback('HomePage.ui updated', null, null, null, null);
}
return { success: true, updated: true };
} catch (error) {
console.error('Error downloading/replacing HomePage.ui:', error);
const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui');
if (fs.existsSync(tempHomePath)) {
fs.unlinkSync(tempHomePath);
}
console.log('HomePage.ui update failed, continuing...');
return { success: false, error: error.message };
}
}
async function downloadAndReplaceLogo(gameDir, progressCallback) {
try {
console.log('Downloading Logo@2x.png from server...');
if (progressCallback) {
progressCallback('Downloading Logo@2x.png...', null, null, null, null);
}
const logoUrl = 'https://files.hytalef2p.com/api/Logo';
const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png');
await downloadFile(logoUrl, tempLogoPath);
const existingLogoPath = findLogoPath(gameDir);
if (existingLogoPath && fs.existsSync(existingLogoPath)) {
console.log('Found existing Logo@2x.png at:', existingLogoPath);
const backupPath = existingLogoPath + '.backup';
if (!fs.existsSync(backupPath)) {
fs.copyFileSync(existingLogoPath, backupPath);
console.log('Original Logo@2x.png backed up');
}
fs.copyFileSync(tempLogoPath, existingLogoPath);
console.log('Logo@2x.png replaced successfully');
} else {
console.log('No existing Logo@2x.png found, skipping replacement');
}
if (fs.existsSync(tempLogoPath)) {
fs.unlinkSync(tempLogoPath);
}
if (progressCallback) {
progressCallback('Logo@2x.png updated', null, null, null, null);
}
return { success: true, updated: true };
} catch (error) {
console.error('Error downloading/replacing Logo@2x.png:', error);
const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png');
if (fs.existsSync(tempLogoPath)) {
fs.unlinkSync(tempLogoPath);
}
console.log('Logo@2x.png update failed, continuing...');
return { success: false, error: error.message };
}
}
module.exports = {
downloadAndReplaceHomePageUI,
findHomePageUIPath,
downloadAndReplaceLogo,
findLogoPath
};

View File

@@ -1,105 +0,0 @@
const path = require('path');
const fs = require('fs');
const { markAsLaunched, loadConfig } = require('../core/config');
const { checkExistingGameInstallation, updateGameFiles } = require('../managers/gameManager');
const { getInstalledClientVersion, getLatestClientVersion } = require('./versionManager');
async function proposeGameUpdate(existingGame, progressCallback) {
try {
console.log('Proposing game update for existing installation...');
if (progressCallback) {
progressCallback('Checking for game updates...', 0, null, null, null);
}
const [installedVersion, latestVersion] = await Promise.all([
getInstalledClientVersion(),
getLatestClientVersion()
]);
console.log(`Existing installation - Installed: ${installedVersion}, Latest: ${latestVersion}`);
const customAppDir = path.join(existingGame.installPath, 'HytaleF2P');
const customCacheDir = path.join(customAppDir, 'cache');
const customToolsDir = path.join(customAppDir, 'butler');
[customCacheDir, customToolsDir].forEach(dir => {
const fs = require('fs');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
if (progressCallback) {
progressCallback('Updating existing game installation...', 20, null, null, null);
}
await updateGameFiles(latestVersion, progressCallback, existingGame.gameDir, customToolsDir, customCacheDir);
if (progressCallback) {
progressCallback('Game update completed successfully', 100, null, null, null);
}
console.log('Existing game installation updated successfully');
return { success: true, updated: true };
} catch (error) {
console.error('Error updating existing game:', error);
if (progressCallback) {
progressCallback(`Update failed: ${error.message}`, -1, null, null, null);
}
throw error;
}
}
async function handleFirstLaunchCheck(progressCallback) {
try {
const config = loadConfig();
if (config.hasLaunchedBefore === true) {
return { isFirstLaunch: false, needsUpdate: false };
}
console.log('First launch detected, checking for existing game installation...');
const existingGame = checkExistingGameInstallation();
if (!existingGame) {
console.log('No existing game installation found');
const hasUserData = config.installPath || config.username || config.javaPath ||
config.chatUsername || config.userUuids ||
Object.keys(config).length > 0;
if (hasUserData) {
console.log('Detected existing user data but no game, marking as launched');
markAsLaunched();
return { isFirstLaunch: false, needsUpdate: false };
} else {
markAsLaunched();
return { isFirstLaunch: true, needsUpdate: false, existingGame: null };
}
}
console.log('Existing game installation found:', {
gameDir: existingGame.gameDir,
hasUserData: existingGame.hasUserData
});
return {
isFirstLaunch: true,
needsUpdate: true,
existingGame: existingGame
};
} catch (error) {
console.error('Error in first launch check:', error);
markAsLaunched();
return { isFirstLaunch: true, needsUpdate: false, error: error.message };
}
}
module.exports = {
proposeGameUpdate,
handleFirstLaunchCheck
};

View File

@@ -1,31 +0,0 @@
const axios = require('axios');
async function getHytaleNews() {
try {
const response = await axios.get('https://launcher.hytale.com/launcher-feed/release/feed.json', {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
timeout: 10000
});
const articles = response.data.articles || [];
return articles.map(article => ({
title: article.title || '',
description: article.description || '',
destUrl: article.dest_url || '',
imageUrl: article.image_url ?
(article.image_url.startsWith('http') ?
article.image_url :
`https://launcher.hytale.com/launcher-feed/release/${article.image_url}`
) : ''
}));
} catch (error) {
console.error('Failed to fetch news:', error.message);
return [];
}
}
module.exports = {
getHytaleNews
};

View File

@@ -1,34 +0,0 @@
const fs = require('fs');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const { PLAYER_ID_FILE, APP_DIR } = require('../core/paths');
function getOrCreatePlayerId() {
try {
if (!fs.existsSync(APP_DIR)) {
fs.mkdirSync(APP_DIR, { recursive: true });
}
if (fs.existsSync(PLAYER_ID_FILE)) {
const data = JSON.parse(fs.readFileSync(PLAYER_ID_FILE, 'utf8'));
if (data.playerId) {
return data.playerId;
}
}
const newPlayerId = uuidv4();
fs.writeFileSync(PLAYER_ID_FILE, JSON.stringify({
playerId: newPlayerId,
createdAt: new Date().toISOString()
}, null, 2));
return newPlayerId;
} catch (error) {
console.error('Error managing player ID:', error);
return uuidv4();
}
}
module.exports = {
getOrCreatePlayerId
};

View File

@@ -1,56 +0,0 @@
const axios = require('axios');
async function getLatestClientVersion() {
try {
console.log('Fetching latest client version from API...');
const response = await axios.get('https://files.hytalef2p.com/api/version_client', {
timeout: 5000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
if (response.data && response.data.client_version) {
const version = response.data.client_version;
console.log(`Latest client version: ${version}`);
return version;
} else {
console.log('Warning: Invalid API response, falling back to default version');
return '4.pwr';
}
} catch (error) {
console.error('Error fetching client version:', error.message);
console.log('Warning: API unavailable, falling back to default version');
return '4.pwr';
}
}
async function getInstalledClientVersion() {
try {
console.log('Fetching installed client version from API...');
const response = await axios.get('https://files.hytalef2p.com/api/clientCheck', {
timeout: 5000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
if (response.data && response.data.client_version) {
const version = response.data.client_version;
console.log(`Installed client version: ${version}`);
return version;
} else {
console.log('Warning: Invalid clientCheck API response');
return null;
}
} catch (error) {
console.error('Error fetching installed client version:', error.message);
console.log('Warning: clientCheck API unavailable');
return null;
}
}
module.exports = {
getLatestClientVersion,
getInstalledClientVersion
};

View File

@@ -1,73 +0,0 @@
const axios = require('axios');
const UPDATE_CHECK_URL = 'https://files.hytalef2p.com/api/version_launcher';
const CURRENT_VERSION = '2.0.2';
const GITHUB_DOWNLOAD_URL = 'https://github.com/amiayweb/Hytale-F2P/';
class UpdateManager {
constructor() {
this.updateAvailable = false;
this.remoteVersion = null;
}
async checkForUpdates() {
try {
console.log('Checking for updates...');
console.log(`Local version: ${CURRENT_VERSION}`);
const response = await axios.get(UPDATE_CHECK_URL, {
timeout: 5000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
if (response.data && response.data.launcher_version) {
this.remoteVersion = response.data.launcher_version;
console.log(`Remote version: ${this.remoteVersion}`);
if (this.remoteVersion !== CURRENT_VERSION) {
this.updateAvailable = true;
console.log('Update available!');
return {
updateAvailable: true,
currentVersion: CURRENT_VERSION,
newVersion: this.remoteVersion,
downloadUrl: GITHUB_DOWNLOAD_URL
};
} else {
console.log('Launcher is up to date');
return {
updateAvailable: false,
currentVersion: CURRENT_VERSION,
newVersion: this.remoteVersion
};
}
} else {
throw new Error('Invalid API response');
}
} catch (error) {
console.error('Error checking for updates:', error.message);
return {
updateAvailable: false,
error: error.message,
currentVersion: CURRENT_VERSION
};
}
}
getDownloadUrl() {
return GITHUB_DOWNLOAD_URL;
}
getUpdateInfo() {
return {
updateAvailable: this.updateAvailable,
currentVersion: CURRENT_VERSION,
remoteVersion: this.remoteVersion,
downloadUrl: this.getDownloadUrl()
};
}
}
module.exports = UpdateManager;

View File

@@ -1,468 +0,0 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const AdmZip = require('adm-zip');
// Domain configuration
const ORIGINAL_DOMAIN = 'hytale.com';
function getTargetDomain() {
if (process.env.HYTALE_AUTH_DOMAIN) {
return process.env.HYTALE_AUTH_DOMAIN;
}
try {
const { getAuthDomain } = require('../core/config');
return getAuthDomain();
} catch (e) {
return 'sanasol.ws';
}
}
const DEFAULT_NEW_DOMAIN = 'sanasol.ws';
/**
* Patches HytaleClient and HytaleServer binaries to replace hytale.com with custom domain
* This allows the game to connect to a custom authentication server
*/
class ClientPatcher {
constructor() {
this.patchedFlag = '.patched_custom';
}
/**
* Get the target domain for patching
*/
getNewDomain() {
const domain = getTargetDomain();
if (domain.length !== ORIGINAL_DOMAIN.length) {
console.warn(`Warning: Domain "${domain}" length (${domain.length}) doesn't match original "${ORIGINAL_DOMAIN}" (${ORIGINAL_DOMAIN.length})`);
console.warn(`Using default domain: ${DEFAULT_NEW_DOMAIN}`);
return DEFAULT_NEW_DOMAIN;
}
return domain;
}
/**
* Convert a string to UTF-16LE bytes (how .NET stores strings)
*/
stringToUtf16LE(str) {
const buf = Buffer.alloc(str.length * 2);
for (let i = 0; i < str.length; i++) {
buf.writeUInt16LE(str.charCodeAt(i), i * 2);
}
return buf;
}
/**
* Convert a string to UTF-8 bytes (how Java stores strings)
*/
stringToUtf8(str) {
return Buffer.from(str, 'utf8');
}
/**
* Find all occurrences of a pattern in a buffer
*/
findAllOccurrences(buffer, pattern) {
const positions = [];
let pos = 0;
while (pos < buffer.length) {
const index = buffer.indexOf(pattern, pos);
if (index === -1) break;
positions.push(index);
pos = index + 1;
}
return positions;
}
/**
* UTF-8 domain replacement for Java JAR files.
* Java stores strings in UTF-8 format in the constant pool.
*/
findAndReplaceDomainUtf8(data, oldDomain, newDomain) {
let count = 0;
const result = Buffer.from(data);
const oldUtf8 = this.stringToUtf8(oldDomain);
const newUtf8 = this.stringToUtf8(newDomain);
const positions = this.findAllOccurrences(result, oldUtf8);
for (const pos of positions) {
newUtf8.copy(result, pos);
count++;
console.log(` Patched UTF-8 occurrence at offset 0x${pos.toString(16)}`);
}
return { buffer: result, count };
}
/**
* Smart domain replacement that handles both null-terminated and non-null-terminated strings.
* .NET AOT stores some strings in various formats:
* - Standard UTF-16LE (each char is 2 bytes with \x00 high byte)
* - Length-prefixed where last char may have metadata byte instead of \x00
*/
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
let count = 0;
const result = Buffer.from(data);
const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1));
const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1));
const oldLastChar = this.stringToUtf16LE(oldDomain.slice(-1));
const newLastChar = this.stringToUtf16LE(newDomain.slice(-1));
const oldLastCharByte = oldDomain.charCodeAt(oldDomain.length - 1);
const newLastCharByte = newDomain.charCodeAt(newDomain.length - 1);
const positions = this.findAllOccurrences(result, oldUtf16NoLast);
for (const pos of positions) {
const lastCharPos = pos + oldUtf16NoLast.length;
if (lastCharPos + 1 > result.length) continue;
const lastCharFirstByte = result[lastCharPos];
if (lastCharFirstByte === oldLastCharByte) {
newUtf16NoLast.copy(result, pos);
result[lastCharPos] = newLastCharByte;
if (lastCharPos + 1 < result.length) {
const secondByte = result[lastCharPos + 1];
if (secondByte === 0x00) {
console.log(` Patched UTF-16LE occurrence at offset 0x${pos.toString(16)}`);
} else {
console.log(` Patched length-prefixed occurrence at offset 0x${pos.toString(16)} (metadata: 0x${secondByte.toString(16)})`);
}
}
count++;
}
}
return { buffer: result, count };
}
/**
* Check if the client binary has already been patched
*/
isPatchedAlready(clientPath) {
const newDomain = this.getNewDomain();
const patchFlagFile = clientPath + this.patchedFlag;
if (fs.existsSync(patchFlagFile)) {
try {
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
if (flagData.targetDomain === newDomain) {
return true;
}
} catch (e) {
}
}
return false;
}
/**
* Mark the client as patched
*/
markAsPatched(clientPath) {
const newDomain = this.getNewDomain();
const patchFlagFile = clientPath + this.patchedFlag;
const flagData = {
patchedAt: new Date().toISOString(),
originalDomain: ORIGINAL_DOMAIN,
targetDomain: newDomain,
patcherVersion: '1.0.0'
};
fs.writeFileSync(patchFlagFile, JSON.stringify(flagData, null, 2));
}
/**
* Create a backup of the original client binary
*/
backupClient(clientPath) {
const backupPath = clientPath + '.original';
if (!fs.existsSync(backupPath)) {
console.log(` Creating backup at ${path.basename(backupPath)}`);
fs.copyFileSync(clientPath, backupPath);
return backupPath;
}
console.log(' Backup already exists');
return backupPath;
}
/**
* Restore the original client binary from backup
*/
restoreClient(clientPath) {
const backupPath = clientPath + '.original';
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, clientPath);
const patchFlagFile = clientPath + this.patchedFlag;
if (fs.existsSync(patchFlagFile)) {
fs.unlinkSync(patchFlagFile);
}
console.log('Client restored from backup');
return true;
}
console.log('No backup found to restore');
return false;
}
/**
* Patch the client binary to use the custom domain
* @param {string} clientPath - Path to the HytaleClient binary
* @param {function} progressCallback - Optional callback for progress updates
* @returns {object} Result object with success status and details
*/
async patchClient(clientPath, progressCallback) {
const newDomain = this.getNewDomain();
console.log('=== Client Patcher ===');
console.log(`Target: ${clientPath}`);
console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`);
if (!fs.existsSync(clientPath)) {
const error = `Client binary not found: ${clientPath}`;
console.error(error);
return { success: false, error };
}
if (this.isPatchedAlready(clientPath)) {
console.log(`Client already patched for ${newDomain}, skipping`);
if (progressCallback) {
progressCallback('Client already patched', 100);
}
return { success: true, alreadyPatched: true, patchCount: 0 };
}
if (progressCallback) {
progressCallback('Preparing to patch client...', 10);
}
console.log('Creating backup...');
this.backupClient(clientPath);
if (progressCallback) {
progressCallback('Reading client binary...', 20);
}
console.log('Reading client binary...');
const data = fs.readFileSync(clientPath);
console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`);
if (progressCallback) {
progressCallback('Patching domain references...', 50);
}
console.log('Patching domain references...');
const { buffer: patchedData, count } = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, newDomain);
if (count === 0) {
console.log('No occurrences of hytale.com found - binary may already be modified or has different format');
return { success: true, patchCount: 0, warning: 'No domain occurrences found' };
}
if (progressCallback) {
progressCallback('Writing patched binary...', 80);
}
console.log('Writing patched binary...');
fs.writeFileSync(clientPath, patchedData);
this.markAsPatched(clientPath);
if (progressCallback) {
progressCallback('Patching complete', 100);
}
console.log(`Successfully patched ${count} occurrences`);
console.log('=== Patching Complete ===');
return { success: true, patchCount: count };
}
/**
* Patch the server JAR to use the custom domain
* JAR files are ZIP archives, so we need to extract, patch class files, and repackage
* @param {string} serverPath - Path to the HytaleServer.jar
* @param {function} progressCallback - Optional callback for progress updates
* @returns {object} Result object with success status and details
*/
async patchServer(serverPath, progressCallback) {
const newDomain = this.getNewDomain();
console.log('=== Server Patcher ===');
console.log(`Target: ${serverPath}`);
console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`);
if (!fs.existsSync(serverPath)) {
const error = `Server JAR not found: ${serverPath}`;
console.error(error);
return { success: false, error };
}
if (this.isPatchedAlready(serverPath)) {
console.log(`Server already patched for ${newDomain}, skipping`);
if (progressCallback) {
progressCallback('Server already patched', 100);
}
return { success: true, alreadyPatched: true, patchCount: 0 };
}
if (progressCallback) {
progressCallback('Preparing to patch server...', 10);
}
console.log('Creating backup...');
this.backupClient(serverPath);
if (progressCallback) {
progressCallback('Extracting server JAR...', 20);
}
console.log('Opening server JAR...');
const zip = new AdmZip(serverPath);
const entries = zip.getEntries();
console.log(`JAR contains ${entries.length} entries`);
if (progressCallback) {
progressCallback('Patching class files...', 40);
}
let totalCount = 0;
const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN);
const newUtf8 = this.stringToUtf8(newDomain);
for (const entry of entries) {
const name = entry.entryName;
if (name.endsWith('.class') || name.endsWith('.properties') ||
name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) {
const data = entry.getData();
if (data.includes(oldUtf8)) {
const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, newDomain);
if (count > 0) {
zip.updateFile(entry.entryName, patchedData);
console.log(` Patched ${count} occurrences in ${name}`);
totalCount += count;
}
}
}
}
if (totalCount === 0) {
console.log('No occurrences of hytale.com found in server JAR entries');
return { success: true, patchCount: 0, warning: 'No domain occurrences found in JAR' };
}
if (progressCallback) {
progressCallback('Writing patched JAR...', 80);
}
console.log('Writing patched JAR...');
zip.writeZip(serverPath);
this.markAsPatched(serverPath);
if (progressCallback) {
progressCallback('Server patching complete', 100);
}
console.log(`Successfully patched ${totalCount} occurrences in server`);
console.log('=== Server Patching Complete ===');
return { success: true, patchCount: totalCount };
}
/**
* Find the client binary path based on platform
*/
findClientPath(gameDir) {
const candidates = [];
if (process.platform === 'darwin') {
candidates.push(path.join(gameDir, 'Client', 'Hytale.app', 'Contents', 'MacOS', 'HytaleClient'));
candidates.push(path.join(gameDir, 'Client', 'HytaleClient'));
} else if (process.platform === 'win32') {
candidates.push(path.join(gameDir, 'Client', 'HytaleClient.exe'));
} else {
candidates.push(path.join(gameDir, 'Client', 'HytaleClient'));
}
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
findServerPath(gameDir) {
const candidates = [
path.join(gameDir, 'Server', 'HytaleServer.jar'),
path.join(gameDir, 'Server', 'server.jar')
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
/**
* Ensure both client and server are patched before launching
* @param {string} gameDir - Path to the game directory
* @param {function} progressCallback - Optional callback for progress updates
*/
async ensureClientPatched(gameDir, progressCallback) {
const results = {
client: null,
server: null,
success: true
};
const clientPath = this.findClientPath(gameDir);
if (clientPath) {
if (progressCallback) {
progressCallback('Patching client binary...', 10);
}
results.client = await this.patchClient(clientPath, (msg, pct) => {
if (progressCallback) {
progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
}
});
} else {
console.warn('Could not find HytaleClient binary');
results.client = { success: false, error: 'Client binary not found' };
}
const serverPath = this.findServerPath(gameDir);
if (serverPath) {
if (progressCallback) {
progressCallback('Patching server JAR...', 50);
}
results.server = await this.patchServer(serverPath, (msg, pct) => {
if (progressCallback) {
progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
}
});
} else {
console.warn('Could not find HytaleServer.jar');
results.server = { success: false, error: 'Server JAR not found' };
}
results.success = (results.client && results.client.success) || (results.server && results.server.success);
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched);
results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0);
if (progressCallback) {
progressCallback('Patching complete', 100);
}
return results;
}
}
module.exports = new ClientPatcher();

View File

@@ -1,211 +0,0 @@
const fs = require('fs');
const path = require('path');
const axios = require('axios');
async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
let lastError = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`);
if (attempt > 0 && progressCallback) {
progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null);
await new Promise(resolve => setTimeout(resolve, 2000 * attempt)); // Délai progressif
}
const response = await axios({
method: 'GET',
url: url,
responseType: 'stream',
timeout: 60000, // 60 secondes timeout
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.9',
'Referer': 'https://launcher.hytale.com/',
'Connection': 'keep-alive'
},
// Configuration Axios pour la robustesse réseau
validateStatus: function (status) {
return status >= 200 && status < 300;
},
// Retry configuration
maxRedirects: 5,
// Network resilience
family: 4 // Force IPv4
});
const totalSize = parseInt(response.headers['content-length'], 10);
let downloaded = 0;
let lastProgressTime = Date.now();
const startTime = Date.now();
// Nettoyer le fichier de destination s'il existe
if (fs.existsSync(dest)) {
fs.unlinkSync(dest);
}
const writer = fs.createWriteStream(dest);
let downloadStalled = false;
let stalledTimeout = null;
response.data.on('data', (chunk) => {
downloaded += chunk.length;
const now = Date.now();
// Reset stalled timer on data received
if (stalledTimeout) {
clearTimeout(stalledTimeout);
}
// Set new stalled timer (30 seconds without data = stalled)
stalledTimeout = setTimeout(() => {
downloadStalled = true;
writer.destroy();
response.data.destroy();
}, 30000);
if (progressCallback && totalSize > 0 && (now - lastProgressTime > 100)) { // Update every 100ms max
const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100));
const elapsed = (now - startTime) / 1000;
const speed = elapsed > 0 ? downloaded / elapsed : 0;
progressCallback(null, percent, speed, downloaded, totalSize);
lastProgressTime = now;
}
});
response.data.on('error', (error) => {
if (stalledTimeout) {
clearTimeout(stalledTimeout);
}
console.error(`Stream error on attempt ${attempt + 1}:`, error.code || error.message);
writer.destroy();
});
response.data.pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', () => {
if (stalledTimeout) {
clearTimeout(stalledTimeout);
}
if (!downloadStalled) {
console.log(`Download completed successfully on attempt ${attempt + 1}`);
resolve();
} else {
reject(new Error('Download stalled'));
}
});
writer.on('error', (error) => {
if (stalledTimeout) {
clearTimeout(stalledTimeout);
}
reject(error);
});
response.data.on('error', (error) => {
if (stalledTimeout) {
clearTimeout(stalledTimeout);
}
reject(error);
});
});
// Si on arrive ici, le téléchargement a réussi
return;
} catch (error) {
lastError = error;
console.error(`Download attempt ${attempt + 1} failed:`, error.code || error.message);
// Nettoyer le fichier partiel en cas d'erreur
if (fs.existsSync(dest)) {
try {
fs.unlinkSync(dest);
} catch (cleanupError) {
console.warn('Could not cleanup partial file:', cleanupError.message);
}
}
// Vérifier si c'est une erreur réseau que l'on peut retry
const retryableErrors = ['ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'EPROTO'];
const isRetryable = retryableErrors.includes(error.code) ||
error.message.includes('timeout') ||
error.message.includes('stalled') ||
(error.response && error.response.status >= 500);
if (!isRetryable || attempt === maxRetries - 1) {
console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`);
break;
}
console.log(`Retryable error detected, will retry in ${2000 * (attempt + 1)}ms...`);
}
}
throw new Error(`Download failed after ${maxRetries} attempts. Last error: ${lastError?.code || lastError?.message || 'Unknown error'}`);
}
function findHomePageUIPath(gameLatest) {
function searchDirectory(dir) {
try {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
if (item.isFile() && item.name === 'HomePage.ui') {
return path.join(dir, item.name);
} else if (item.isDirectory()) {
const found = searchDirectory(path.join(dir, item.name));
if (found) {
return found;
}
}
}
} catch (error) {
}
return null;
}
if (!fs.existsSync(gameLatest)) {
return null;
}
return searchDirectory(gameLatest);
}
function findLogoPath(gameLatest) {
function searchDirectory(dir) {
try {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
if (item.isFile() && item.name === 'Logo@2x.png') {
return path.join(dir, item.name);
} else if (item.isDirectory()) {
const found = searchDirectory(path.join(dir, item.name));
if (found) {
return found;
}
}
}
} catch (error) {
}
return null;
}
if (!fs.existsSync(gameLatest)) {
return null;
}
return searchDirectory(gameLatest);
}
module.exports = {
downloadFile,
findHomePageUIPath,
findLogoPath
};

View File

@@ -1,73 +0,0 @@
const { execSync } = require('child_process');
function getOS() {
if (process.platform === 'win32') return 'windows';
if (process.platform === 'darwin') return 'darwin';
if (process.platform === 'linux') return 'linux';
return 'unknown';
}
function getArch() {
return process.arch === 'x64' ? 'amd64' : process.arch;
}
function isWaylandSession() {
if (process.platform !== 'linux') {
return false;
}
const sessionType = process.env.XDG_SESSION_TYPE;
if (sessionType && sessionType.toLowerCase() === 'wayland') {
return true;
}
if (process.env.WAYLAND_DISPLAY) {
return true;
}
try {
const sessionId = process.env.XDG_SESSION_ID;
if (sessionId) {
const output = execSync(`loginctl show-session ${sessionId} -p Type`, { encoding: 'utf8' });
if (output && output.toLowerCase().includes('wayland')) {
return true;
}
}
} catch (err) {
}
return false;
}
function setupWaylandEnvironment() {
if (process.platform !== 'linux') {
return {};
}
if (!isWaylandSession()) {
console.log('Detected X11 session, using default environment');
return {};
}
console.log('Detected Wayland session, configuring environment...');
const envVars = {
SDL_VIDEODRIVER: 'wayland',
GDK_BACKEND: 'wayland',
QT_QPA_PLATFORM: 'wayland',
MOZ_ENABLE_WAYLAND: '1',
_JAVA_AWT_WM_NONREPARENTING: '1'
};
envVars.ELECTRON_OZONE_PLATFORM_HINT = 'wayland';
console.log('Wayland environment variables:', envVars);
return envVars;
}
module.exports = {
getOS,
getArch,
isWaylandSession,
setupWaylandEnvironment
};

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

858
main.js
View File

@@ -1,858 +0,0 @@
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const path = require('path');
const fs = require('fs');
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, isGameInstalled, uninstallGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
const UpdateManager = require('./backend/updateManager');
const logger = require('./backend/logger');
const profileManager = require('./backend/managers/profileManager');
logger.interceptConsole();
let mainWindow;
let updateManager;
let discordRPC = null;
// Discord Rich Presence setup
const DISCORD_CLIENT_ID = '1462244937868513373';
function initDiscordRPC() {
try {
// Check if Discord RPC is enabled in settings
const rpcEnabled = loadDiscordRPC();
if (!rpcEnabled) {
console.log('Discord RPC disabled in settings');
return;
}
const { Client } = require('discord-rpc');
discordRPC = new Client({ transport: 'ipc' });
discordRPC.on('ready', () => {
console.log('Discord RPC connected');
setDiscordActivity();
});
discordRPC.on('disconnected', () => {
console.log('Discord RPC disconnected');
});
discordRPC.login({ clientId: DISCORD_CLIENT_ID }).catch(err => {
console.log('Failed to connect to Discord:', err.message);
});
} catch (error) {
console.log('Discord RPC module not available:', error.message);
}
}
function setDiscordActivity() {
if (!discordRPC) return;
try {
discordRPC.setActivity({
details: 'Using HytaleF2P',
startTimestamp: Date.now(),
largeImageKey: 'hytale_logo',
largeImageText: 'Hytale F2P Launcher',
buttons: [
{
label: 'GitHub',
url: 'https://github.com/amiayweb/Hytale-F2P'
}
]
});
} catch (error) {
console.error('Failed to set Discord activity:', error.message);
}
}
function toggleDiscordRPC(enabled) {
console.log('Toggling Discord RPC:', enabled);
if (enabled && !discordRPC) {
console.log('Initializing Discord RPC...');
initDiscordRPC();
} else if (!enabled && discordRPC) {
try {
console.log('Disconnecting Discord RPC...');
discordRPC.clearActivity();
discordRPC.destroy();
discordRPC = null;
console.log('Discord RPC disconnected successfully');
} catch (error) {
console.error('Error disconnecting Discord RPC:', error.message);
discordRPC = null; // Force null même en cas d'erreur
}
}
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 720,
frame: false,
resizable: false,
alwaysOnTop: false,
backgroundColor: '#090909',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
devTools: false,
webSecurity: true
}
});
mainWindow.loadFile('GUI/index.html');
// Cleanup Discord RPC when window is closed
mainWindow.on('closed', () => {
console.log('Main window closed, cleaning up Discord RPC...');
cleanupDiscordRPC();
});
// Initialize Discord Rich Presence
initDiscordRPC();
updateManager = new UpdateManager();
setTimeout(async () => {
const updateInfo = await updateManager.checkForUpdates();
if (updateInfo.updateAvailable) {
mainWindow.webContents.send('show-update-popup', updateInfo);
}
}, 3000);
//mainWindow.webContents.openDevTools();
mainWindow.webContents.on('devtools-opened', () => {
mainWindow.webContents.closeDevTools();
});
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.control && input.shift && input.key.toLowerCase() === 'i') {
event.preventDefault();
}
if (input.control && input.shift && input.key.toLowerCase() === 'j') {
event.preventDefault();
}
if (input.control && input.shift && input.key.toLowerCase() === 'c') {
event.preventDefault();
}
if (input.key === 'F12') {
event.preventDefault();
}
if (input.key === 'F5') {
event.preventDefault();
}
});
mainWindow.webContents.on('context-menu', (e) => {
e.preventDefault();
});
mainWindow.webContents.setIgnoreMenuShortcuts(true);
}
app.whenReady().then(async () => {
console.log('=== HYTALE F2P LAUNCHER STARTED ===');
console.log('Platform:', process.platform);
console.log('Architecture:', process.arch);
console.log('Electron version:', process.versions.electron);
console.log('Node.js version:', process.versions.node);
console.log('Log directory:', logger.getLogDirectory());
// Initialize Profile Manager (runs migration if needed)
profileManager.init();
createWindow();
setTimeout(async () => {
let timeoutReached = false;
const unlockPlayButton = () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('lock-play-button', false);
}
};
const timeoutId = setTimeout(() => {
timeoutReached = true;
console.warn('First launch check timeout reached, unlocking play button');
unlockPlayButton();
}, 15000);
try {
console.log('Starting first launch check...');
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('lock-play-button', true);
}
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('first-launch-progress', { message, percent, speed, downloaded, total });
}
};
const firstLaunchResult = await Promise.race([
handleFirstLaunchCheck(progressCallback),
new Promise((_, reject) => {
setTimeout(() => reject(new Error('First launch check timeout')), 12000);
})
]);
clearTimeout(timeoutId);
if (timeoutReached) {
console.log('Timeout already reached, skipping result processing');
return;
}
console.log('First launch check result:', firstLaunchResult);
if (mainWindow && !mainWindow.isDestroyed()) {
if (firstLaunchResult.needsUpdate && firstLaunchResult.existingGame) {
console.log('Sending show-first-launch-update event...');
setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('show-first-launch-update', {
existingGame: firstLaunchResult.existingGame,
isFirstLaunch: firstLaunchResult.isFirstLaunch
});
}
}, 1000);
} else if (firstLaunchResult.isFirstLaunch && !firstLaunchResult.existingGame) {
console.log('Sending show-first-launch-welcome event...');
setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('show-first-launch-welcome');
}
}, 1000);
} else {
unlockPlayButton();
}
}
} catch (error) {
clearTimeout(timeoutId);
console.error('Error during first launch check:', error);
if (!timeoutReached) {
unlockPlayButton();
}
}
}, 3000);
});
function cleanupDiscordRPC() {
if (discordRPC) {
try {
console.log('Cleaning up Discord RPC...');
discordRPC.clearActivity();
setTimeout(() => {
try {
discordRPC.destroy();
} catch (error) {
console.log('Error during final Discord RPC cleanup:', error.message);
}
}, 100);
discordRPC = null;
} catch (error) {
console.log('Error cleaning up Discord RPC:', error.message);
discordRPC = null;
}
}
}
app.on('before-quit', () => {
console.log('=== LAUNCHER BEFORE QUIT ===');
cleanupDiscordRPC();
});
app.on('window-all-closed', () => {
console.log('=== LAUNCHER CLOSING ===');
cleanupDiscordRPC();
if (process.platform !== 'darwin') {
app.quit();
}
});
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath) => {
try {
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
const data = {
message: message || null,
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
speed: speed !== null && speed !== undefined ? speed : null,
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
total: total !== null && total !== undefined ? total : null
};
mainWindow.webContents.send('progress-update', data);
}
};
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath);
return result;
} catch (error) {
console.error('Launch error:', error);
const errorMessage = error.message || error.toString();
if (mainWindow && !mainWindow.isDestroyed()) {
setTimeout(() => {
mainWindow.webContents.send('progress-complete');
}, 2000);
}
return { success: false, error: errorMessage };
}
});
ipcMain.handle('install-game', async (event, playerName, javaPath, installPath) => {
try {
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
const data = {
message: message || null,
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
speed: speed !== null && speed !== undefined ? speed : null,
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
total: total !== null && total !== undefined ? total : null
};
mainWindow.webContents.send('progress-update', data);
}
};
const result = await installGame(playerName, progressCallback, javaPath, installPath);
return result;
} catch (error) {
console.error('Install error:', error);
const errorMessage = error.message || error.toString();
return { success: false, error: errorMessage };
}
});
ipcMain.handle('save-username', (event, username) => {
saveUsername(username);
return { success: true };
});
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);
return { success: true };
});
ipcMain.handle('load-java-path', () => {
return loadJavaPath();
});
ipcMain.handle('save-install-path', (event, installPath) => {
saveInstallPath(installPath);
logger.updateInstallPath();
return { success: true };
});
ipcMain.handle('load-install-path', () => {
return loadInstallPath();
});
ipcMain.handle('save-discord-rpc', (event, enabled) => {
saveDiscordRPC(enabled);
toggleDiscordRPC(enabled);
return { success: true };
});
ipcMain.handle('load-discord-rpc', () => {
return loadDiscordRPC();
});
ipcMain.handle('select-install-path', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'],
title: 'Select Installation Folder'
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
ipcMain.handle('accept-first-launch-update', async (event, existingGame) => {
try {
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
const data = {
message: message || null,
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
speed: speed !== null && speed !== undefined ? speed : null,
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
total: total !== null && total !== undefined ? total : null
};
mainWindow.webContents.send('first-launch-progress', data);
}
};
const result = await proposeGameUpdate(existingGame, progressCallback);
return result;
} catch (error) {
console.error('First launch update error:', error);
const errorMessage = error.message || error.toString();
return { success: false, error: errorMessage };
}
});
ipcMain.handle('mark-as-launched', async () => {
try {
markAsLaunched();
return { success: true };
} catch (error) {
console.error('Mark as launched error:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('is-game-installed', async () => {
try {
return await Promise.race([
Promise.resolve(isGameInstalled()),
new Promise((resolve) => setTimeout(() => resolve(false), 5000))
]);
} catch (error) {
console.error('Error checking game installation:', error);
return false;
}
});
ipcMain.handle('uninstall-game', async () => {
try {
await uninstallGame();
return { success: true };
} catch (error) {
console.error('Uninstall error:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-hytale-news', async () => {
try {
const news = await getHytaleNews();
return news;
} catch (error) {
console.error('News fetch error:', error);
return [];
}
});
ipcMain.handle('open-external', async (event, url) => {
try {
await shell.openExternal(url);
return { success: true };
} catch (error) {
console.error('Failed to open external URL:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('open-game-location', async () => {
try {
const { getResolvedAppDir } = require('./backend/launcher');
const gameDir = path.join(getResolvedAppDir(), 'release', 'package', 'game');
if (fs.existsSync(gameDir)) {
await shell.openPath(gameDir);
return { success: true };
} else {
throw new Error('Game directory not found');
}
} catch (error) {
console.error('Failed to open game location:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('browse-java-path', async () => {
const isWindows = process.platform === 'win32';
const isMac = process.platform === 'darwin';
let dialogOptions;
if (isWindows) {
dialogOptions = {
properties: ['openFile'],
title: 'Select Java Executable',
filters: [
{ name: 'Java Executable', extensions: ['exe'] },
{ name: 'All Files', extensions: ['*'] }
]
};
} else if (isMac) {
dialogOptions = {
properties: ['openFile'],
title: 'Select Java Executable',
message: 'Select java executable (usually in /Library/Java/JavaVirtualMachines/*/Contents/Home/bin/java)',
filters: [
{ name: 'All Files', extensions: ['*'] }
]
};
} else {
dialogOptions = {
properties: ['openFile'],
title: 'Select Java Executable',
message: 'Select java executable (usually /usr/bin/java or similar)',
filters: [
{ name: 'All Files', extensions: ['*'] }
]
};
}
const result = await dialog.showOpenDialog(mainWindow, dialogOptions);
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths[0];
}
return null;
});
ipcMain.handle('save-settings', async (event, settings) => {
try {
if (settings.playerName) saveUsername(settings.playerName);
if (settings.javaPath !== undefined) saveJavaPath(settings.javaPath);
if (settings.installPath !== undefined) {
saveInstallPath(settings.installPath);
logger.updateInstallPath();
}
return { success: true };
} catch (error) {
console.error('Save settings error:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('load-settings', async () => {
try {
return {
playerName: loadUsername() || 'Player',
javaPath: loadJavaPath() || '',
installPath: loadInstallPath() || '',
customInstall: false
};
} catch (error) {
console.error('Load settings error:', error);
return {
playerName: 'Player',
javaPath: '',
installPath: '',
customInstall: false
};
}
});
const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher');
const os = require('os');
ipcMain.handle('get-local-app-data', async () => {
return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
});
ipcMain.handle('get-user-id', async () => {
try {
const { getOrCreatePlayerId } = require('./backend/launcher');
return await getOrCreatePlayerId();
} catch (error) {
console.error('Error getting user ID:', error);
return null;
}
});
ipcMain.handle('load-installed-mods', async (event, modsPath) => {
try {
return await loadInstalledMods(modsPath);
} catch (error) {
console.error('Error loading installed mods:', error);
return [];
}
});
ipcMain.handle('openExternalLink', async (event, url) => {
try {
console.log('Opening external URL:', url);
await shell.openExternal(url);
return { success: true };
} catch (error) {
console.error('Error opening external link:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('download-mod', async (event, modInfo) => {
try {
return await downloadMod(modInfo);
} catch (error) {
console.error('Error downloading mod:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('uninstall-mod', async (event, modId, modsPath) => {
try {
return await uninstallMod(modId, modsPath);
} catch (error) {
console.error('Error uninstalling mod:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('toggle-mod', async (event, modId, modsPath) => {
try {
return await toggleMod(modId, modsPath);
} catch (error) {
console.error('Error toggling mod:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-mods-path', async () => {
try {
return await getModsPath();
} catch (error) {
console.error('Error getting mods path:', error);
return null;
}
});
ipcMain.handle('select-mod-files', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile', 'multiSelections'],
title: 'Select Mod Files',
filters: [
{ name: 'Mod Files', extensions: ['jar', 'zip'] },
{ name: 'All Files', extensions: ['*'] }
]
});
if (!result.canceled && result.filePaths.length > 0) {
return result.filePaths;
}
return null;
});
ipcMain.handle('copy-mod-file', async (event, sourcePath, modsPath) => {
try {
const fileName = path.basename(sourcePath);
const destPath = path.join(modsPath, fileName);
fs.copyFileSync(sourcePath, destPath);
return { success: true, fileName };
} catch (error) {
console.error('Error copying mod file:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('check-for-updates', async () => {
try {
return await updateManager.checkForUpdates();
} catch (error) {
console.error('Error checking for updates:', error);
return { updateAvailable: false, error: error.message };
}
});
ipcMain.handle('open-download-page', async () => {
try {
await shell.openExternal(updateManager.getDownloadUrl());
setTimeout(() => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
}, 1000);
return { success: true };
} catch (error) {
console.error('Error opening download page:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-update-info', async () => {
return updateManager.getUpdateInfo();
});
ipcMain.handle('window-close', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();
}
});
ipcMain.handle('window-minimize', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.minimize();
}
});
ipcMain.handle('get-log-directory', () => {
return logger.getLogDirectory();
});
ipcMain.handle('get-current-uuid', async () => {
try {
return getCurrentUuid();
} catch (error) {
console.error('Error getting current UUID:', error);
return null;
}
});
ipcMain.handle('get-all-uuid-mappings', async () => {
try {
const mappings = getAllUuidMappings();
return Object.entries(mappings).map(([username, uuid]) => ({
username,
uuid,
isCurrent: username === require('./backend/launcher').loadUsername()
}));
} catch (error) {
console.error('Error getting UUID mappings:', error);
return [];
}
});
ipcMain.handle('set-uuid-for-user', async (event, username, uuid) => {
try {
await setUuidForUser(username, uuid);
return { success: true };
} catch (error) {
console.error('Error setting UUID for user:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('generate-new-uuid', async () => {
try {
return generateNewUuid();
} catch (error) {
console.error('Error generating new UUID:', error);
return null;
}
});
ipcMain.handle('delete-uuid-for-user', async (event, username) => {
try {
const result = deleteUuidForUser(username);
return { success: result };
} catch (error) {
console.error('Error deleting UUID for user:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('reset-current-user-uuid', async () => {
try {
const newUuid = resetCurrentUserUuid();
return { success: true, uuid: newUuid };
} catch (error) {
console.error('Error resetting current user UUID:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
try {
const logDir = logger.getLogDirectory();
if (!logDir) return null;
const files = fs.readdirSync(logDir)
.filter(file => file.startsWith('launcher-') && file.endsWith('.log'))
.map(file => ({
name: file,
path: path.join(logDir, file),
mtime: fs.statSync(path.join(logDir, file)).mtime
}))
.sort((a, b) => b.mtime - a.mtime);
if (files.length === 0) return null;
const latestLogFile = files[0].path;
const content = fs.readFileSync(latestLogFile, 'utf8');
const lines = content.split('\n');
return lines.slice(-maxLines).join('\n');
} catch (error) {
console.error('Error reading logs:', error);
return null;
}
});
// Profile Management IPC
ipcMain.handle('profile-create', async (event, name) => {
try {
return profileManager.createProfile(name);
} catch (error) {
return { error: error.message };
}
});
ipcMain.handle('profile-list', async () => {
return profileManager.getProfiles();
});
ipcMain.handle('profile-get-active', async () => {
return profileManager.getActiveProfile();
});
ipcMain.handle('profile-activate', async (event, id) => {
try {
return await profileManager.activateProfile(id);
} catch (error) {
return { error: error.message };
}
});
ipcMain.handle('profile-delete', async (event, id) => {
try {
return profileManager.deleteProfile(id);
} catch (error) {
return { error: error.message };
}
});
ipcMain.handle('profile-update', async (event, id, updates) => {
try {
return profileManager.updateProfile(id, updates);
} catch (error) {
return { error: error.message };
}
});

4955
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,123 +0,0 @@
{
"name": "hytale-f2p-launcherv2",
"version": "2.0.1",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
"homepage": "https://github.com/amiayweb/Hytale-F2P",
"main": "main.js",
"scripts": {
"start": "electron .",
"dev": "electron . --dev",
"build": "electron-builder",
"build:win": "electron-builder --win",
"build:linux": "electron-builder --linux",
"build:mac": "electron-builder --mac",
"build:all": "electron-builder --win --linux --mac"
},
"keywords": [
"hytale",
"launcher",
"game",
"client",
"cross-platform",
"electron",
"auto-update",
"mod-manager",
"chat"
],
"author": {
"name": "AMIAY",
"email": "support@amiay.dev"
},
"license": "MIT",
"devDependencies": {
"electron": "^40.0.0",
"electron-builder": "^26.4.0"
},
"dependencies": {
"adm-zip": "^0.5.10",
"axios": "^1.6.0",
"discord-rpc": "^4.0.1",
"tar": "^6.2.1",
"uuid": "^9.0.1"
},
"overrides": {
"tar": "$tar"
},
"build": {
"appId": "com.hytalef2p.launcher",
"productName": "Hytale F2P",
"directories": {
"output": "dist"
},
"files": [
"main.js",
"preload.js",
"backend/**/*",
"GUI/**/*",
"package.json"
],
"win": {
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
},
{
"target": "portable",
"arch": [
"x64"
]
}
],
"icon": "icon.ico"
},
"linux": {
"target": [
{
"target": "AppImage",
"arch": [
"x64"
]
},
{
"target": "deb",
"arch": [
"x64"
]
}
],
"icon": "build/icon.png",
"category": "Game",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support"
},
"mac": {
"target": [
{
"target": "dmg",
"arch": [
"universal"
]
},
{
"target": "zip",
"arch": [
"universal"
]
}
],
"icon": "build/icon.icns",
"category": "public.app-category.games"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
}

View File

@@ -1,87 +0,0 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
launchGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('launch-game', playerName, javaPath, installPath),
installGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath),
closeWindow: () => ipcRenderer.invoke('window-close'),
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
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),
loadInstallPath: () => ipcRenderer.invoke('load-install-path'),
saveDiscordRPC: (enabled) => ipcRenderer.invoke('save-discord-rpc', enabled),
loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'),
selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
uninstallGame: () => ipcRenderer.invoke('uninstall-game'),
getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'),
openExternal: (url) => ipcRenderer.invoke('open-external', url),
openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url),
openGameLocation: () => ipcRenderer.invoke('open-game-location'),
saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings),
loadSettings: () => ipcRenderer.invoke('load-settings'),
getLocalAppData: () => ipcRenderer.invoke('get-local-app-data'),
getModsPath: () => ipcRenderer.invoke('get-mods-path'),
loadInstalledMods: (modsPath) => ipcRenderer.invoke('load-installed-mods', modsPath),
downloadMod: (modInfo) => ipcRenderer.invoke('download-mod', modInfo),
uninstallMod: (modId, modsPath) => ipcRenderer.invoke('uninstall-mod', modId, modsPath),
toggleMod: (modId, modsPath) => ipcRenderer.invoke('toggle-mod', modId, modsPath),
selectModFiles: () => ipcRenderer.invoke('select-mod-files'),
copyModFile: (sourcePath, modsPath) => ipcRenderer.invoke('copy-mod-file', sourcePath, modsPath),
onProgressUpdate: (callback) => {
ipcRenderer.on('progress-update', (event, data) => callback(data));
},
onProgressComplete: (callback) => {
ipcRenderer.on('progress-complete', () => 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) => {
ipcRenderer.on('show-update-popup', (event, data) => callback(data));
},
acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame),
markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'),
onFirstLaunchUpdate: (callback) => {
ipcRenderer.on('show-first-launch-update', (event, data) => callback(data));
},
onFirstLaunchWelcome: (callback) => {
ipcRenderer.on('show-first-launch-welcome', () => callback());
},
onFirstLaunchProgress: (callback) => {
ipcRenderer.on('first-launch-progress', (event, data) => callback(data));
},
onLockPlayButton: (callback) => {
ipcRenderer.on('lock-play-button', (event, locked) => callback(locked));
},
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
// UUID Management methods
getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'),
getAllUuidMappings: () => ipcRenderer.invoke('get-all-uuid-mappings'),
setUuidForUser: (username, uuid) => ipcRenderer.invoke('set-uuid-for-user', username, uuid),
generateNewUuid: () => ipcRenderer.invoke('generate-new-uuid'),
deleteUuidForUser: (username) => ipcRenderer.invoke('delete-uuid-for-user', username),
resetCurrentUserUuid: () => ipcRenderer.invoke('reset-current-user-uuid'),
// Profile API
profile: {
create: (name) => ipcRenderer.invoke('profile-create', name),
list: () => ipcRenderer.invoke('profile-list'),
getActive: () => ipcRenderer.invoke('profile-get-active'),
activate: (id) => ipcRenderer.invoke('profile-activate', id),
delete: (id) => ipcRenderer.invoke('profile-delete', id),
update: (id, updates) => ipcRenderer.invoke('profile-update', id, updates)
}
});