Compare commits

..

31 Commits

Author SHA1 Message Date
sanasol
e7a033932f v2.3.2: fix truncated download cache, update Discord link
Fix pre-release downloads failing with "unexpected EOF" by validating
cached PWR file sizes against manifest expected sizes. Previously only
checked > 1MB which accepted truncated files. Also update Discord
invite link to new server across all files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 15:14:20 +01:00
sanasol
11c6d40dfe chore: remove private docs from repo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 14:38:34 +01:00
sanasol
0dafb17c7b v2.3.1: CDN redirect gateway, fix token username bug
- Migrate patch downloads to auth server redirect gateway (302 -> CDN)
  Allows instant CDN switching via admin panel without launcher update
- Fix identity token "Player" username mismatch on fresh install
  Add token username verification with retry in fetchAuthTokens
- Refactor versionManager to use mirror manifest via auth.sanasol.ws/patches
- Add optimal patch routing (BFS) for differential updates
- Add PATCH_CDN_INFRASTRUCTURE.md documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 14:36:09 +01:00
sanasol
66112f15b2 fix: use placeholder publish URL (runtime resolver overrides it)
The actual update URL is resolved dynamically via Forgejo API
in _resolveUpdateUrl() before each update check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:08:30 +01:00
sanasol
0a71fdac8c v2.3.0: migrate auto-update to Forgejo, add Arch build
- Switch auto-update from GitHub to Forgejo (generic provider)
- Dynamically resolve latest release URL via Forgejo API
- Add pacman target to Linux builds
- Hide direct upload URL in repository secret

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:55:35 +01:00
sanasol
4b9eae215b ci: add Arch Linux pacman build and hide upload URL
- Add pacman target to electron-builder Linux build
- Upload .pacman packages to release
- FORGEJO_UPLOAD now uses repository secret

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:45:07 +01:00
sanasol
1510eceb0f Hide direct upload IP in repository secret
Move FORGEJO_UPLOAD URL from hardcoded value to ${{ secrets.FORGEJO_UPLOAD_URL }}
to avoid exposing server IP when repo goes public.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:35:53 +01:00
sanasol
c4b5368538 fix: use direct Forgejo port 3001 for uploads (bypass Traefik+Cloudflare)
- FORGEJO_UPLOAD now uses http://208.69.78.130:3001 (plain HTTP direct to Forgejo)
- Removed -sk flags and Host headers (not needed for plain HTTP)
- Added --max-time 600 for large file uploads
- Cloudflare 100MB limit and Traefik HTTP/2 stream errors both bypassed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 02:24:17 +01:00
sanasol
e0ebf137fc fix: add Host header for direct IP uploads to route to Forgejo
Without Host header, Traefik routes direct IP requests to the
default backend (hytale-auth) instead of Forgejo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 02:03:15 +01:00
sanasol
5241a502e5 fix: disable npmRebuild for Windows cross-compilation
ELECTRON_BUILDER_SKIP_NATIVE_REBUILD env var not recognized by
electron-builder 26.6.0. Use --config.npmRebuild=false CLI flag
to skip register-scheme native module rebuild.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 01:35:59 +01:00
sanasol
3e7c7ccff3 fix: use domain for API calls, direct IP only for file uploads
Cloudflare runners can't reach direct IP. Split into two env vars:
- FORGEJO_API (domain) for release creation and ID lookups
- FORGEJO_UPLOAD (direct IP) for large file uploads >100MB

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 01:33:21 +01:00
sanasol
89d09f032f fix: bypass Cloudflare 100MB upload limit for release assets
Use direct server IP for Forgejo API calls to avoid Cloudflare
proxy rejecting large file uploads (413 Payload Too Large).
macOS DMG/ZIP artifacts are ~350MB each.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 01:16:50 +01:00
sanasol
8bdb78d1e2 ci: fix Windows cross-compilation and add cloudsol runner
- Skip native module rebuild for Windows (register-scheme can't cross-compile)
- ELECTRON_BUILDER_SKIP_NATIVE_REBUILD=true for Windows builds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 01:12:33 +01:00
sanasol
e9e66dbca7 Merge branch 'refactor/bytebuddy-agent' into develop
# Conflicts:
#	.github/workflows/release.yml
2026-02-20 01:01:29 +01:00
sanasol
92a0a26251 ci: fix Forgejo Actions compatibility
- Remove upload-artifact/download-artifact (not supported on Forgejo)
- Each build job uploads directly to release via API
- Add `rpm` package to Linux build dependencies
- Remove separate release job, replaced by create-release + per-job upload
- Remove arch build job entirely

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 00:59:44 +01:00
sanasol
3abe885ab4 ci: adapt release workflow for Forgejo
- Windows build cross-compiles from ubuntu-latest using Wine
- Arch build disabled (commented out)
- Release action switched to actions/forgejo-release@v2
- Removed arch artifacts from release

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 00:54:58 +01:00
AMIAY
82f1dd2739 Merge branch 'main' into develop 2026-02-11 10:48:06 +01:00
AMIAY
4502c11bd0 Use new version API and default to v8 PWR 2026-02-11 10:43:18 +01:00
AMIAY
bcc7476322 Bump package version to 2.2.2
Update package.json version from 2.2.1 to 2.2.2 to mark a patch release.
2026-02-11 09:29:12 +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
Alex
b93dc027e1 Merge pull request #277 from sanasol/refactor/bytebuddy-agent
refactor: Replace pre-patched JAR with ByteBuddy runtime agent
2026-02-08 21:17:36 +07:00
sanasol
fdbca6b9da 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>
2026-02-08 11:44:13 +01:00
Fazri Gading
b2f65bd524 Update SERVER.md 2026-02-06 14:33:22 +08:00
Fazri Gading
bd6b05d1e4 Update SERVER.md official accounts info
Added CloudNord hosting information and new section for playing online with official accounts.
2026-02-06 05:11:00 +08:00
Fazri Gading
454ca7f075 Revise and enhance Hytale F2P Server Guide
Updated the Hytale F2P Server Guide with new sections and improved formatting.
2026-02-06 03:48:17 +08:00
Alex
e7324eb176 Merge pull request #268 from amiayweb/macos-notarization
feat(macos): add code signing and notarization support
2026-02-03 17:06:27 +07: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
18 changed files with 785 additions and 413 deletions

View File

@@ -36,7 +36,7 @@ This Code of Conduct applies within all community spaces, and also applies when
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Discord Server, message Founders/Devs](https://discord.gg/hf2pdc). All complaints will be reviewed and investigated promptly and fairly. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Discord Server, message Founders/Devs](https://discord.gg/Fhbb9Yk5WW). All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident. All community leaders are obligated to respect the privacy and security of the reporter of any incident.

View File

@@ -22,7 +22,7 @@ body:
value: | value: |
If you need help or support with using the launcher, please fill out this support request. If you need help or support with using the launcher, please fill out this support request.
Provide as much detail as possible so we can assist you effectively. Provide as much detail as possible so we can assist you effectively.
**Need a quick assistance?** Please Open-A-Ticket in our [Discord Server](https://discord.gg/gME8rUy3MB)! **Need a quick assistance?** Please Open-A-Ticket in our [Discord Server](https://discord.gg/Fhbb9Yk5WW)!
- type: textarea - type: textarea
id: question id: question

View File

@@ -6,173 +6,117 @@ on:
- 'v*' - 'v*'
workflow_dispatch: workflow_dispatch:
env:
# Domain for small API calls (goes through Cloudflare - fine for <100MB)
FORGEJO_API: https://git.sanhost.net/api/v1
# Direct upload URL (bypasses Cloudflare for large files) - set in repo secrets
FORGEJO_UPLOAD: ${{ secrets.FORGEJO_UPLOAD_URL }}
jobs: jobs:
build-windows: create-release:
runs-on: windows-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Create Draft Release
run: |
curl -s -X POST "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases" \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${{ github.ref_name }}\",\"name\":\"${{ github.ref_name }}\",\"body\":\"Release ${{ github.ref_name }}\",\"draft\":true,\"prerelease\":false}" \
-o release.json
cat release.json
echo "RELEASE_ID=$(cat release.json | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')" >> $GITHUB_ENV
build-windows:
needs: [create-release]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Wine for cross-compilation
run: |
sudo dpkg --add-architecture i386
sudo mkdir -pm755 /etc/apt/keyrings
sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key
sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/$(lsb_release -cs)/winehq-$(lsb_release -cs).sources
sudo apt-get update
sudo apt-get install -y --install-recommends winehq-stable
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: '22' node-version: '22'
cache: 'npm'
- run: npm ci - run: npm ci
- name: Build Windows Packages - name: Build Windows Packages
run: npx electron-builder --win --publish never run: npx electron-builder --win --publish never --config.npmRebuild=false
- uses: actions/upload-artifact@v4
with: - name: Upload to Release
name: windows-builds run: |
path: | RELEASE_ID=$(curl -s "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
dist/*.exe -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
dist/*.exe.blockmap for file in dist/*.exe dist/*.exe.blockmap dist/latest.yml; do
dist/latest.yml [ -f "$file" ] || continue
echo "Uploading $file..."
curl -s --max-time 600 -X POST "${FORGEJO_UPLOAD}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=$(basename $file)" \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-F "attachment=@${file}" || echo "Failed to upload $file"
done
build-macos: build-macos:
needs: [create-release]
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: '22' node-version: '22'
cache: 'npm'
- run: npm ci - run: npm ci
- name: Build macOS Packages - name: Build macOS Packages
env: env:
# Code signing
CSC_LINK: ${{ secrets.CSC_LINK }} CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
# Notarization
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: npx electron-builder --mac --publish never run: npx electron-builder --mac --publish never
- uses: actions/upload-artifact@v4
with: - name: Upload to Release
name: macos-builds run: |
path: | RELEASE_ID=$(curl -s "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
dist/*.dmg -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
dist/*.zip for file in dist/*.dmg dist/*.zip dist/*.blockmap dist/latest-mac.yml; do
dist/*.blockmap [ -f "$file" ] || continue
dist/latest-mac.yml echo "Uploading $file..."
curl -s --max-time 600 -X POST "${FORGEJO_UPLOAD}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=$(basename $file)" \
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
-F "attachment=@${file}" || echo "Failed to upload $file"
done
build-linux: build-linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [create-release]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install build dependencies - name: Install build dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libarchive-tools sudo apt-get install -y libarchive-tools rpm
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: '22' node-version: '22'
cache: 'npm'
- run: npm ci - run: npm ci
- name: Build Linux Packages - name: Build Linux Packages
run: npx electron-builder --linux AppImage deb rpm pacman --publish never
- name: Upload to Release
run: | run: |
npx electron-builder --linux AppImage deb rpm --publish never RELEASE_ID=$(curl -s "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
- uses: actions/upload-artifact@v4 -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
with: for file in dist/*.AppImage dist/*.AppImage.blockmap dist/*.deb dist/*.rpm dist/*.pacman dist/latest-linux.yml; do
name: linux-builds [ -f "$file" ] || continue
path: | echo "Uploading $file..."
dist/*.AppImage curl -s --max-time 600 -X POST "${FORGEJO_UPLOAD}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=$(basename $file)" \
dist/*.AppImage.blockmap -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
dist/*.deb -F "attachment=@${file}" || echo "Failed to upload $file"
dist/*.rpm done
dist/latest-linux.yml
build-arch:
runs-on: ubuntu-latest
container:
image: archlinux:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install base packages
run: |
pacman -Syu --noconfirm
pacman -S --noconfirm \
base-devel \
git \
nodejs \
npm \
rpm-tools \
libxcrypt-compat
- name: Create build user
run: |
useradd -m builder
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
- name: Fix Permissions
run: chown -R builder:builder .
- name: Build Arch Package
run: |
sudo -u builder bash << 'EOF'
set -e
makepkg --printsrcinfo > .SRCINFO
makepkg -s --noconfirm
EOF
- name: Fix permissions for upload
if: always()
run: |
sudo chown -R $(id -u):$(id -g) .
- name: Upload Arch Package
uses: actions/upload-artifact@v4
with:
name: arch-package
path: |
*.pkg.tar.zst
.SRCINFO
include-hidden-files: true
release:
needs: [build-windows, build-macos, build-linux, build-arch]
runs-on: ubuntu-latest
if: |
startsWith(github.ref, 'refs/tags/v') ||
github.ref == 'refs/heads/main' ||
github.event_name == 'workflow_dispatch'
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Display structure of downloaded files
run: ls -R artifacts
- name: Get version from package.json
id: pkg_version
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
files: |
artifacts/arch-package/*.pkg.tar.zst
artifacts/arch-package/.SRCINFO
artifacts/linux-builds/**/*
artifacts/windows-builds/**/*
artifacts/macos-builds/**/*
generate_release_notes: true
draft: true
prerelease: false

3
.gitignore vendored
View File

@@ -17,6 +17,9 @@ dist/
# Project Specific: Downloaded patcher (from hytale-auth-server) # Project Specific: Downloaded patcher (from hytale-auth-server)
backend/patcher/ backend/patcher/
# Private docs (local only)
docs/PATCH_CDN_INFRASTRUCTURE.md
# macOS Specific # macOS Specific
.DS_Store .DS_Store
*.zst.DS_Store *.zst.DS_Store

View File

@@ -53,7 +53,7 @@ window.closeDiscordPopup = function() {
}; };
window.joinDiscord = async function() { window.joinDiscord = async function() {
await window.electronAPI?.openExternal('https://discord.gg/hf2pdc'); await window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
try { try {
await window.electronAPI?.saveConfig({ discordPopup: true }); await window.electronAPI?.saveConfig({ discordPopup: true });

View File

@@ -1103,7 +1103,7 @@ function getRetryContextMessage() {
} }
window.openDiscordExternal = function() { window.openDiscordExternal = function() {
window.electronAPI?.openExternal('https://discord.gg/hf2pdc'); window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
}; };
window.toggleMaximize = toggleMaximize; window.toggleMaximize = toggleMaximize;

View File

@@ -18,7 +18,7 @@
### ⚠️ **WARNING: READ [QUICK START](#-quick-start) before Downloading & Installing the Launcher!** ⚠️ ### ⚠️ **WARNING: READ [QUICK START](#-quick-start) before Downloading & Installing the Launcher!** ⚠️
#### 🛑 **Found a problem? [Join the HF2P Discord](https://discord.gg/hf2pdc) and head to `#-⚠️-community-help`** 🛑 #### 🛑 **Found a problem? [Join the HF2P Discord](https://discord.gg/Fhbb9Yk5WW) and head to `#-⚠️-community-help`** 🛑
<p> <p>
👍 If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> ☕<br> 👍 If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> ☕<br>
@@ -455,7 +455,7 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
<div align="center"> <div align="center">
**Questions? Ads? Collaboration? Endorsement? Other business-related?** **Questions? Ads? Collaboration? Endorsement? Other business-related?**
Message the founders at https://discord.gg/hf2pdc Message the founders at https://discord.gg/Fhbb9Yk5WW
</div> </div>

128
SERVER.md
View File

@@ -2,19 +2,20 @@
Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup. Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup.
### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/hf2pdc** ### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/Fhbb9Yk5WW**
**Table of Contents** **Table of Contents**
* [\[NEW!\] Play Online with Official Accounts 🆕](#new-play-online-with-official-accounts-)
* ["Server" Term and Definition](#server-term-and-definiton) * ["Server" Term and Definition](#server-term-and-definiton)
* [Server Directory Location](#server-directory-location) * [Server Directory Location](#server-directory-location)
* [A. Online Play Feature](#a-online-play-feature) * [A. Host Your Singleplayer World](#a-host-your-singleplayer-world)
* [1. Host Your Singleplayer World using In-Game Invite Code](#1-host-your-singleplayer-world-using-in-game-invite-code) * [1. Using Online-Play Feature In-Game Invite Code](#1-using-online-play-feature--in-game-invite-code)
* [Common Issues (UPnP/NAT/STUN) on Online Play](#common-issues-upnpnatstun-on-online-play) * [Common Issues (UPnP/NAT/STUN) on Online Play](#common-issues-upnpnatstun-on-online-play)
* [2. Host Your Singleplayer World using Tailscale](#2-host-your-singleplayer-world-using-tailscale) * [2. Using Tailscale](#2-using-tailscale)
* [3. Using Radmin VPN](#3-using-radmin-vpn)
* [B. Local Dedicated Server](#b-local-dedicated-server) * [B. Local Dedicated Server](#b-local-dedicated-server)
* [1. Using Playit.gg (Recommended) ✅](#1-using-playitgg-recommended-) * [1. Using Playit.gg (Recommended) ✅](#1-using-playitgg-recommended-)
* [2. Using Radmin VPN](#2-using-radmin-vpn)
* [C. 24/7 Dedicated Server (Advanced)](#c-247-dedicated-server-advanced) * [C. 24/7 Dedicated Server (Advanced)](#c-247-dedicated-server-advanced)
* [Step 1: Get the Files Ready](#step-1-get-the-files-ready) * [Step 1: Get the Files Ready](#step-1-get-the-files-ready)
* [Step 2: Place HytaleServer.jar in the Server directory](#step-2-place-hytaleserverjar-in-the-server-directory) * [Step 2: Place HytaleServer.jar in the Server directory](#step-2-place-hytaleserverjar-in-the-server-directory)
@@ -32,6 +33,69 @@ Play with friends online! This guide covers both easy in-game hosting and advanc
* [10. Getting Help](#10-getting-help) * [10. Getting Help](#10-getting-help)
--- ---
<div align='center'>
<h3>
<b>
Do you want to create Hytale Game Server with EASY SETUP, AFFORDABLE PRICE, AND 24/7 SUPPORT?
</b>
</h3>
<h2>
<b>
<a href="https://cloudnord.net/hytale-server-hosting">CLOUDNORD</a> is the ANSWER! HF2P Server is available!
</b>
</h2>
</div>
**CloudNord's Hytale, Minecraft, and Game Hosting** is at the core of our Server Hosting business. Join our Gaming community and experience our large choice of premium game servers, weve got you covered with super high-performance hardware, fantastic support options, and powerful server hosting to build and explore your worlds without limits!
**Order your Hytale, Minecraft, or other game servers today!**
Choose Java Edition, Bedrock Edition, Cross-Play, or any of our additional supported games.
Enjoy **20% OFF** all new game servers, **available now for a limited time!** Dont miss out.
### **CloudNord key hosting features include:**
- Instant Server Setup ⚡
- High Performance Game Servers 🚀
- Game DDoS Protection 🛡️
- Intelligent Game Backups 🧠
- Quick Modpack Installer 🔧
- Quick Plugin & Mod Installer 🧰
- Full File Access 🗃️
- 24/7 Support 📞 🏪
- Powerful Game Control Server Panel 💪
### **Check Us Out:**
* 👉 CloudNord Website: https://cloudnord.net/hytalef2p
* 👉 CloudNord Discord: https://discord.gg/TYxGrmUz4Y
* 👉 CloudNord Reviews: https://www.trustpilot.com/review/cloudnord.net?page=2&stars=5
---
### [NEW!] Play Online with Official Accounts 🆕
**Documentations:**
* [Hytale-Server-Docker by Sanasol](https://github.com/sanasol/hytale-server-docker/tree/main?tab=readme-ov-file#dual-authentication)
**Requirements:**
* Using the patched HytaleServer.jar
* Has Official Account with Purchased status on Official Hytale Website.
* This official account holder can be the server hoster or one of the players.
**Steps:**
1. Running the patched HytaleServer.jar with either [B. Local Dedicated Server](#b-local-dedicated-server) or [C. 24/7 Dedicated Server (Advanced)](#c-247-dedicated-server-advanced) successfully.
2. On the server's console/terminal/CMD, server admin **MUST RUN THIS EACH BOOT** to allow players with Official Hytale game license to connect on the server:
```
/auth logout
/auth persistence Encrypted
/auth login device
```
3. Server console will show instructions, an URL and a code; these will be revoked after 10 minutes if not authorized.
4. The server hoster can open the URL directly to browser by holding Ctrl then Click on it, or copy and send it to the player with official account.
5. Once it authorized, the official accounts can join server with F2P players.
6. If you want to modify anything, look at the [Hytale-Server-Docker](https://github.com/sanasol/hytale-server-docker/) above, give the repo a STAR too.
---
### "Server" Term and Definiton ### "Server" Term and Definiton
"HytaleServer.jar", which called as "Server", functions as the place of authentication of the client that supposed to go to Hytale Official Authentication System but we managed our way to redirect it on our service (Thanks to Sanasol), handling approximately thousands of players worldwide to play this game for free. "HytaleServer.jar", which called as "Server", functions as the place of authentication of the client that supposed to go to Hytale Official Authentication System but we managed our way to redirect it on our service (Thanks to Sanasol), handling approximately thousands of players worldwide to play this game for free.
@@ -41,14 +105,15 @@ Kindly support us via [our Buy Me a Coffee link](https://buymeacoffee.com/hf2p)
### Server Directory Location ### Server Directory Location
Here are the directory locations of Server folder if you have installed Here are the directory locations of Server folder if you have installed it on default instalation location:
- **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server` - **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server`
- **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server` - **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
- **Linux:** `~/.hytalef2p/release/package/game/latest/Server` - **Linux:** `~/.hytalef2p/release/package/game/latest/Server`
> [!NOTE] > [!NOTE]
> This location only exists if the user installed the game using our launcher. The `Server` folder needed to auth the HytaleClient to play Hytale online > This location only exists if the user installed the game using our launcher.
> (for now; we planned to add offline mode in later version of our launcher). > The `Server` folder needed to auth the HytaleClient to play Hytale in Singleplayer/Multiplayer for now.
> (We planned to add offline mode in later version of our launcher).
> [!IMPORTANT] > [!IMPORTANT]
> Hosting a dedicated Hytale server will not need the exact similar tree. You can put it anywhere, as long as the directory has `Assets.zip` which > Hosting a dedicated Hytale server will not need the exact similar tree. You can put it anywhere, as long as the directory has `Assets.zip` which
@@ -64,6 +129,7 @@ Terms and conditions applies.
## 1. Using Online-Play Feature / In-Game Invite Code ## 1. Using Online-Play Feature / In-Game Invite Code
The easiest way to play with friends - no manual server setup required! The easiest way to play with friends - no manual server setup required!
*The game automatically handles networking using UPnP/STUN/NAT traversal.* *The game automatically handles networking using UPnP/STUN/NAT traversal.*
**For Online Play to work, you need:** **For Online Play to work, you need:**
@@ -112,6 +178,7 @@ Warning: Your network configuration may prevent other players from connecting.
</details> </details>
<details><summary><b>b. "UPnP Failed" or "Port Mapping Failed" Warning</b></summary> <details><summary><b>b. "UPnP Failed" or "Port Mapping Failed" Warning</b></summary>
**Check your router:** **Check your router:**
1. Log into router admin panel (usually `192.168.1.1` or `192.168.0.1`) 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") 2. Find UPnP settings (often under "Advanced" or "NAT")
@@ -123,7 +190,8 @@ Warning: Your network configuration may prevent other players from connecting.
- See "Port Forwarding" or "Workarounds or NAT/CGNAT" sections below - See "Port Forwarding" or "Workarounds or NAT/CGNAT" sections below
</details> </details>
<details><summary><b>c. "Strict NAT" or "Symmetric NAT" Warning</b></summary> <details><summary><b>c. "Connected via STUN", "Strict NAT" or "Symmetric NAT" Warning</b></summary>
Some routers have restrictive NAT that blocks peer connections. Some routers have restrictive NAT that blocks peer connections.
**Try:** **Try:**
@@ -133,6 +201,7 @@ Some routers have restrictive NAT that blocks peer connections.
</details> </details>
## 2. Using Tailscale ## 2. Using Tailscale
Tailscale creates mesh VPN service that streamlines connecting devices and services securely across different networks. And **works crossplatform!!** Tailscale creates mesh VPN service that streamlines connecting devices and services securely across different networks. And **works crossplatform!!**
1. All members are required to download [Tailscale](https://tailscale.com/download) on your device. 1. All members are required to download [Tailscale](https://tailscale.com/download) on your device.
@@ -148,6 +217,17 @@ Tailscale creates mesh VPN service that streamlines connecting devices and servi
* Use the new share code to connect * Use the new share code to connect
* To test your connection, ping the host's ipv4 mentioned in Tailscale * To test your connection, ping the host's ipv4 mentioned in Tailscale
## 3. Using Radmin VPN
Creates a virtual LAN - all players need to install it:
1. Download [Radmin VPN](https://www.radmin-vpn.com/) - All players install it
2. One person create a room/network, others join with network name/password
3. Host joined the world, others will connect to it.
4. Open Hytale Game > Servers > Add Servers > Direct Connect > Type IP Address of the Host from Radmin.
These options bypass all NAT/CGNAT issues. But for **Windows machines only!**
--- ---
# B. Local Dedicated Server # B. Local Dedicated Server
@@ -167,11 +247,12 @@ Free tunneling service - only the host needs to install it:
* Right-click file > Properties > Turn on 'Executable as a Program' | or `chmod +x playit-linux-amd64` on terminal * Right-click file > Properties > Turn on 'Executable as a Program' | or `chmod +x playit-linux-amd64` on terminal
* Run by double-clicking the file or `./playit-linux-amd64` via terminal * Run by double-clicking the file or `./playit-linux-amd64` via terminal
5. Open the URL/link by `Ctrl+Click` it. If unable, select the URL, then Right-Click to Copy (`Ctrl+Shift+C` for Linux) then Paste the URL into your browser to link it with your created account. 5. Open the URL/link by `Ctrl+Click` it. If unable, select the URL, then Right-Click to Copy (`Ctrl+Shift+C` for Linux) then Paste the URL into your browser to link it with your created account.
6. **WARNING: Do not close the terminal if you are still playing or hosting the server** 6. Once it done, download the `run_server_with_tokens (1)` script file (`.BAT` for Windows, `.SH` for Linux) from our Discord server > channel `#open-public-server`
7. Once it done, download the `run_server_with_tokens` script file (`.BAT` for Windows, `.SH` for Linux) from our Discord server > channel `#open-public-server` 7. Put the script file to the `Server` folder in `HytaleF2P` directory (`%localappdata%\HytaleF2P\release\package\game\latest\Server`)
8. Put the script file to the `Server` folder in `HytaleF2P` directory (`%localappdata%\HytaleF2P\release\package\game\latest\Server`) 8. Rename the script file to `run_server_with_tokens` to make it easier if you run it with Terminal, then do Method A or B.
9. Copy the `Assets.zip` from the `%localappdata%\HytaleF2P\release\package\game\latest\` folder to the `Server\` folder. (TIP: You can use Symlink of that file to reduce disk usage!) 9. If you put it in `Server` folder in `HytaleF2P` launcher, change `ASSETS_PATH="${ASSETS_PATH:-./Assets.zip}"` inside the script to be `ASSETS_PATH="${ASSETS_PATH:-../Assets.zip}"`. NOTICE THE `./` and `../` DIFFERENCE.
10. Double-click the .BAT file to host your server, wait until it shows: 10. Copy the `Assets.zip` from the `%localappdata%\HytaleF2P\release\package\game\latest\` folder to the `Server\` folder. (TIP: You can use Symlink of that file to reduce disk usage!)
11. Double-click the .BAT file to host your server, wait until it shows:
``` ```
=================================================== ===================================================
Hytale Server Booted! [Multiplayer, Fresh Universe] Hytale Server Booted! [Multiplayer, Fresh Universe]
@@ -180,16 +261,12 @@ Hytale Server Booted! [Multiplayer, Fresh Universe]
11. Connect to the server by go to `Servers` in your game client, press `Add Server`, type `localhost` in the address box, use any name for your server. 11. Connect to the server by go to `Servers` in your game client, press `Add Server`, type `localhost` in the address box, use any name for your server.
12. Send the public address in Step 3 to your friends. 12. Send the public address in Step 3 to your friends.
## 2. Using Radmin VPN > [!CAUTION]
> Do not close the Playit.gg Terminal OR HytaleServer Terminal if you are still playing or hosting the server.
Creates a virtual LAN - all players need to install it: ## 2. Using Tailscale [DRAFT]
1. Download [Radmin VPN](https://www.radmin-vpn.com/) - All players install it Tailscale
2. One person create a room/network, others join with network name/password
3. Host joined the world, others will connect to it.
4. Open Hytale Game > Servers > Add Servers > Direct Connect > Type IP Address of the Host from Radmin.
These options bypass all NAT/CGNAT issues. But for **Windows machines only!**
--- ---
@@ -228,12 +305,12 @@ For 24/7 servers, custom configurations, or hosting on a VPS/dedicated machine.
**Windows:** **Windows:**
```batch ```batch
run_server.bat run_server_with_token.bat
``` ```
**macOS / Linux:** **macOS / Linux:**
```bash ```bash
./run_server.sh ./run_server_with_token.sh
``` ```
--- ---
@@ -503,3 +580,6 @@ See [Docker documentation](https://github.com/Hybrowse/hytale-server-docker) for
- Auth Server: sanasol.ws - Auth Server: sanasol.ws

View File

@@ -1,6 +1,6 @@
# Hytale F2P Launcher - Troubleshooting Guide # Hytale F2P Launcher - Troubleshooting Guide
This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [Discord](https://discord.gg/gME8rUy3MB). This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [Discord](https://discord.gg/Fhbb9Yk5WW).
--- ---
@@ -437,7 +437,7 @@ Game sessions have a 10-hour TTL. This is by design for security.
If your issue isn't resolved by this guide: If your issue isn't resolved by this guide:
1. **Check existing issues:** [GitHub Issues](https://github.com/amiayweb/Hytale-F2P/issues) 1. **Check existing issues:** [GitHub Issues](https://github.com/amiayweb/Hytale-F2P/issues)
2. **Join Discord:** [discord.gg/gME8rUy3MB](https://discord.gg/gME8rUy3MB) 2. **Join Discord:** [discord.gg/Fhbb9Yk5WW](https://discord.gg/Fhbb9Yk5WW)
3. **Open a new issue** with: 3. **Open a new issue** with:
- Your operating system and version - Your operating system and version
- Launcher version - Launcher version

View File

@@ -4,6 +4,10 @@ const logger = require('./logger');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const os = require('os'); const os = require('os');
const https = require('https');
const FORGEJO_API = 'https://git.sanhost.net/api/v1';
const FORGEJO_REPO = 'sanasol/hytale-f2p';
class AppUpdater { class AppUpdater {
constructor(mainWindow) { constructor(mainWindow) {
@@ -14,6 +18,34 @@ class AppUpdater {
this.setupAutoUpdater(); this.setupAutoUpdater();
} }
/**
* Fetch the latest non-draft release tag from Forgejo and set the feed URL
*/
async _resolveUpdateUrl() {
return new Promise((resolve, reject) => {
https.get(`${FORGEJO_API}/repos/${FORGEJO_REPO}/releases?limit=5`, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => {
try {
const releases = JSON.parse(data);
const latest = releases.find(r => !r.draft && !r.prerelease);
if (latest) {
const url = `https://git.sanhost.net/${FORGEJO_REPO}/releases/download/${latest.tag_name}`;
console.log(`Auto-update URL resolved to: ${url}`);
autoUpdater.setFeedURL({ provider: 'generic', url });
resolve(url);
} else {
reject(new Error('No published release found'));
}
} catch (e) {
reject(e);
}
});
}).on('error', reject);
});
}
setupAutoUpdater() { setupAutoUpdater() {
// Configure logger for electron-updater // Configure logger for electron-updater
@@ -216,8 +248,10 @@ class AppUpdater {
} }
checkForUpdatesAndNotify() { checkForUpdatesAndNotify() {
// Check for updates and notify if available // Resolve latest release URL then check for updates
autoUpdater.checkForUpdatesAndNotify().catch(err => { this._resolveUpdateUrl().catch(err => {
console.warn('Failed to resolve update URL:', err.message);
}).then(() => autoUpdater.checkForUpdatesAndNotify()).catch(err => {
console.error('Failed to check for updates:', err); console.error('Failed to check for updates:', err);
// Network errors are not critical - just log and continue // Network errors are not critical - just log and continue
@@ -245,8 +279,10 @@ class AppUpdater {
} }
checkForUpdates() { checkForUpdates() {
// Manual check for updates (returns promise) // Manual check - resolve latest release URL first
return autoUpdater.checkForUpdates().catch(err => { return this._resolveUpdateUrl().catch(err => {
console.warn('Failed to resolve update URL:', err.message);
}).then(() => autoUpdater.checkForUpdates()).catch(err => {
console.error('Failed to check for updates:', err); console.error('Failed to check for updates:', err);
// Network errors are not critical - just return no update available // Network errors are not critical - just return no update available

View File

@@ -1,5 +1,5 @@
const FORCE_CLEAN_INSTALL_VERSION = false; const FORCE_CLEAN_INSTALL_VERSION = false;
const CLEAN_INSTALL_TEST_VERSION = '4.pwr'; const CLEAN_INSTALL_TEST_VERSION = 'v4';
module.exports = { module.exports = {
FORCE_CLEAN_INSTALL_VERSION, FORCE_CLEAN_INSTALL_VERSION,

View File

@@ -3,7 +3,7 @@ const path = require('path');
const { execFile } = require('child_process'); const { execFile } = require('child_process');
const { downloadFile, retryDownload } = require('../utils/fileManager'); const { downloadFile, retryDownload } = require('../utils/fileManager');
const { getOS, getArch } = require('../utils/platformUtils'); const { getOS, getArch } = require('../utils/platformUtils');
const { validateChecksum, extractVersionDetails, canUseDifferentialUpdate, needsIntermediatePatches, getInstalledClientVersion } = require('../services/versionManager'); const { validateChecksum, extractVersionDetails, getInstalledClientVersion, getUpdatePlan, extractVersionNumber } = require('../services/versionManager');
const { installButler } = require('./butlerManager'); const { installButler } = require('./butlerManager');
const { GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths'); const { GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
const { saveVersionClient } = require('../core/config'); const { saveVersionClient } = require('../core/config');
@@ -156,15 +156,15 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
console.log(`Initiating intelligent update to version ${targetVersion}`); console.log(`Initiating intelligent update to version ${targetVersion}`);
const currentVersion = getInstalledClientVersion(); const currentVersion = getInstalledClientVersion();
console.log(`Current version: ${currentVersion || 'none (clean install)'}`); const currentBuild = extractVersionNumber(currentVersion) || 0;
console.log(`Target version: ${targetVersion}`); const targetBuild = extractVersionNumber(targetVersion);
console.log(`Branch: ${branch}`); console.log(`Current build: ${currentBuild}, Target build: ${targetBuild}, Branch: ${branch}`);
// For non-release branches, always do full install
if (branch !== 'release') { if (branch !== 'release') {
console.log(`Pre-release branch detected - forcing full archive download`); console.log('Pre-release branch detected - forcing full archive download');
const versionDetails = await extractVersionDetails(targetVersion, branch); const versionDetails = await extractVersionDetails(targetVersion, branch);
const archiveName = path.basename(versionDetails.fullUrl); const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
if (progressCallback) { if (progressCallback) {
progressCallback('Downloading full game archive (pre-release)...', 0, null, null, null); progressCallback('Downloading full game archive (pre-release)...', 0, null, null, null);
@@ -177,14 +177,14 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
return; return;
} }
if (!currentVersion) { // Clean install (no current version)
if (currentBuild === 0) {
console.log('No existing installation detected - downloading full archive'); console.log('No existing installation detected - downloading full archive');
const versionDetails = await extractVersionDetails(targetVersion, branch); const versionDetails = await extractVersionDetails(targetVersion, branch);
const archiveName = path.basename(versionDetails.fullUrl); const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
if (progressCallback) { if (progressCallback) {
progressCallback(`Downloading full game archive (first install - v${targetVersion})...`, 0, null, null, null); progressCallback(`Downloading full game archive (first install - v${targetBuild})...`, 0, null, null, null);
} }
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback); await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
@@ -194,59 +194,67 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
return; return;
} }
const patchesToApply = needsIntermediatePatches(currentVersion, targetVersion); // Already at target
if (currentBuild >= targetBuild) {
if (patchesToApply.length === 0) { console.log('Already at target version or newer');
console.log('Already at target version or invalid version sequence');
return; return;
} }
console.log(`Applying ${patchesToApply.length} differential patch(es): ${patchesToApply.join(' -> ')}`); // Use mirror's update plan for optimal patch routing
try {
const plan = await getUpdatePlan(currentBuild, targetBuild, branch);
for (let i = 0; i < patchesToApply.length; i++) { console.log(`Applying ${plan.steps.length} patch(es): ${plan.steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')}`);
const patchVersion = patchesToApply[i];
const versionDetails = await extractVersionDetails(patchVersion, branch);
const canDifferential = canUseDifferentialUpdate(getInstalledClientVersion(), versionDetails); for (let i = 0; i < plan.steps.length; i++) {
const step = plan.steps[i];
if (!canDifferential || !versionDetails.differentialUrl) { const stepName = `${step.from}_to_${step.to}`;
console.log(`WARNING: Differential patch not available for ${patchVersion}, using full archive`); const archivePath = path.join(cacheDir, `${branch}_${stepName}.pwr`);
const archiveName = path.basename(versionDetails.fullUrl); const isDifferential = step.from !== 0;
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
if (progressCallback) { if (progressCallback) {
progressCallback(`Downloading full archive for ${patchVersion} (${i + 1}/${patchesToApply.length})...`, 0, null, null, null); progressCallback(`Downloading patch ${i + 1}/${plan.steps.length}: ${stepName}...`, 0, null, null, null);
} }
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback); await acquireGameArchive(step.url, archivePath, null, progressCallback);
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
} else {
console.log(`Applying differential patch: ${versionDetails.sourceVersion} -> ${patchVersion}`);
const archiveName = path.basename(versionDetails.differentialUrl);
const archivePath = path.join(cacheDir, `${branch}_patch_${archiveName}`);
if (progressCallback) { if (progressCallback) {
progressCallback(`Applying patch ${i + 1}/${patchesToApply.length}: ${patchVersion}...`, 0, null, null, null); progressCallback(`Applying patch ${i + 1}/${plan.steps.length}: ${stepName}...`, 50, null, null, null);
} }
await acquireGameArchive(versionDetails.differentialUrl, archivePath, versionDetails.checksum, progressCallback); await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, isDifferential);
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, true);
// Clean up patch file
if (fs.existsSync(archivePath)) { if (fs.existsSync(archivePath)) {
try { try {
fs.unlinkSync(archivePath); fs.unlinkSync(archivePath);
console.log(`Cleaned up patch file: ${archiveName}`); console.log(`Cleaned up: ${stepName}.pwr`);
} catch (cleanupErr) { } catch (cleanupErr) {
console.warn(`Failed to cleanup patch file: ${cleanupErr.message}`); console.warn(`Failed to cleanup: ${cleanupErr.message}`);
} }
} }
saveVersionClient(`v${step.to}`);
console.log(`Patch ${stepName} applied (${i + 1}/${plan.steps.length})`);
} }
saveVersionClient(patchVersion); console.log(`Update completed. Version ${targetVersion} is now installed.`);
console.log(`Patch ${patchVersion} applied successfully (${i + 1}/${patchesToApply.length})`); } catch (planError) {
} console.error('Update plan failed:', planError.message);
console.log('Falling back to full archive download');
console.log(`Update completed successfully. Version ${targetVersion} is now installed.`); // Fallback: full install
const versionDetails = await extractVersionDetails(targetVersion, branch);
const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
if (progressCallback) {
progressCallback(`Downloading full game archive (fallback)...`, 0, null, null, null);
}
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
saveVersionClient(targetVersion);
}
} }
async function ensureGameInstalled(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) { async function ensureGameInstalled(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) {

View File

@@ -61,12 +61,39 @@ async function fetchAuthTokens(uuid, name) {
} }
const data = await response.json(); const data = await response.json();
console.log('Auth tokens received from server'); const identityToken = data.IdentityToken || data.identityToken;
const sessionToken = data.SessionToken || data.sessionToken;
return { // Verify the identity token has the correct username
identityToken: data.IdentityToken || data.identityToken, // This catches cases where the auth server defaults to "Player"
sessionToken: data.SessionToken || data.sessionToken try {
}; const parts = identityToken.split('.');
if (parts.length >= 2) {
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
if (payload.username && payload.username !== name && name !== 'Player') {
console.warn(`[Auth] Token username mismatch: token has "${payload.username}", expected "${name}". Retrying...`);
// Retry once with explicit name
const retryResponse = 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 (retryResponse.ok) {
const retryData = await retryResponse.json();
console.log('[Auth] Retry successful');
return {
identityToken: retryData.IdentityToken || retryData.identityToken,
sessionToken: retryData.SessionToken || retryData.sessionToken
};
}
}
}
} catch (verifyErr) {
console.warn('[Auth] Token verification skipped:', verifyErr.message);
}
console.log('Auth tokens received from server');
return { identityToken, sessionToken };
} catch (error) { } catch (error) {
console.error('Failed to fetch auth tokens:', error.message); console.error('Failed to fetch auth tokens:', error.message);
// Fallback to local generation if server unavailable // Fallback to local generation if server unavailable

View File

@@ -5,7 +5,7 @@ const { promisify } = require('util');
const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursive, GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths'); const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursive, GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
const { getOS, getArch } = require('../utils/platformUtils'); const { getOS, getArch } = require('../utils/platformUtils');
const { downloadFile, retryDownload, retryStalledDownload, MAX_AUTOMATIC_STALL_RETRIES } = require('../utils/fileManager'); const { downloadFile, retryDownload, retryStalledDownload, MAX_AUTOMATIC_STALL_RETRIES } = require('../utils/fileManager');
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager'); const { getLatestClientVersion, getInstalledClientVersion, getUpdatePlan, extractVersionNumber } = require('../services/versionManager');
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig'); const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
const { installButler } = require('./butlerManager'); const { installButler } = require('./butlerManager');
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager'); const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
@@ -64,7 +64,7 @@ async function safeRemoveDirectory(dirPath, maxRetries = 3) {
} }
} }
async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) { async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback, cacheDir = CACHE_DIR, manualRetry = false, directUrl = null, expectedSize = null) {
const osName = getOS(); const osName = getOS();
const arch = getArch(); const arch = getArch();
@@ -72,28 +72,68 @@ async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallb
throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.'); throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.');
} }
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${branch}/0/${fileName}`; let url;
const dest = path.join(cacheDir, `${branch}_${fileName}`);
// Check if file exists and validate it if (directUrl) {
if (fs.existsSync(dest) && !manualRetry) { url = directUrl;
console.log('PWR file found in cache:', dest); console.log(`[DownloadPWR] Using direct URL: ${url}`);
} else {
// Validate file size (PWR files should be > 1MB and >= 1.5GB for complete downloads) const { getPWRUrl } = require('../services/versionManager');
const stats = fs.statSync(dest); try {
if (stats.size < 1024 * 1024) { console.log(`[DownloadPWR] Fetching mirror URL for branch: ${branch}, version: ${fileName}`);
return false; url = await getPWRUrl(branch, fileName);
} console.log(`[DownloadPWR] Mirror URL: ${url}`);
} catch (error) {
// Check if file is under 1.5 GB (incomplete download) console.error(`[DownloadPWR] Failed to get mirror URL: ${error.message}`);
const sizeInMB = stats.size / 1024 / 1024; const { MIRROR_BASE_URL } = require('../services/versionManager');
if (sizeInMB < 1500) { url = `${MIRROR_BASE_URL}/${osName}/${arch}/${branch}/0_to_${extractVersionNumber(fileName)}.pwr`;
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`); console.log(`[DownloadPWR] Fallback URL: ${url}`);
return false;
} }
} }
console.log('Fetching PWR patch file:', url); // Look up expected file size from manifest if not provided
if (!expectedSize) {
try {
const { fetchMirrorManifest } = require('../services/versionManager');
const manifest = await fetchMirrorManifest();
// Try to match: "0_to_11" format or "v11" format
const versionMatch = fileName.match(/^(\d+)_to_(\d+)$/);
let manifestKey;
if (versionMatch) {
manifestKey = `${osName}/${arch}/${branch}/${fileName}.pwr`;
} else {
const buildNum = extractVersionNumber(fileName);
manifestKey = `${osName}/${arch}/${branch}/0_to_${buildNum}.pwr`;
}
if (manifest.files[manifestKey]) {
expectedSize = manifest.files[manifestKey].size;
console.log(`[PWR] Expected size from manifest: ${(expectedSize / 1024 / 1024).toFixed(2)} MB`);
}
} catch (e) {
console.log(`[PWR] Could not fetch expected size from manifest: ${e.message}`);
}
}
const dest = path.join(cacheDir, `${branch}_${fileName}.pwr`);
// Check if file exists and validate it
if (fs.existsSync(dest) && !manualRetry) {
const stats = fs.statSync(dest);
if (stats.size > 1024 * 1024) {
// Validate against expected size - reject if file is truncated (< 99% of expected)
if (expectedSize && stats.size < expectedSize * 0.99) {
console.log(`[PWR] Cached file truncated: ${(stats.size / 1024 / 1024).toFixed(2)} MB, expected ${(expectedSize / 1024 / 1024).toFixed(2)} MB. Deleting and re-downloading.`);
fs.unlinkSync(dest);
} else {
console.log(`[PWR] Using cached file: ${dest} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
return dest;
}
} else {
console.log(`[PWR] Cached file too small (${stats.size} bytes), re-downloading`);
}
}
console.log(`[DownloadPWR] Downloading from: ${url}`);
try { try {
if (manualRetry) { if (manualRetry) {
@@ -119,7 +159,7 @@ async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallb
const retryStats = fs.statSync(dest); const retryStats = fs.statSync(dest);
console.log(`PWR file downloaded (auto-retry), size: ${(retryStats.size / 1024 / 1024).toFixed(2)} MB`); console.log(`PWR file downloaded (auto-retry), size: ${(retryStats.size / 1024 / 1024).toFixed(2)} MB`);
if (!validatePWRFile(dest)) { if (!validatePWRFile(dest, expectedSize)) {
console.log(`[PWR Validation] PWR file validation failed after auto-retry, deleting corrupted file: ${dest}`); console.log(`[PWR Validation] PWR file validation failed after auto-retry, deleting corrupted file: ${dest}`);
fs.unlinkSync(dest); fs.unlinkSync(dest);
throw new Error('Downloaded PWR file is corrupted or invalid after automatic retry. Please retry manually'); throw new Error('Downloaded PWR file is corrupted or invalid after automatic retry. Please retry manually');
@@ -170,7 +210,7 @@ async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallb
const stats = fs.statSync(dest); const stats = fs.statSync(dest);
console.log(`PWR file downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`); console.log(`PWR file downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
if (!validatePWRFile(dest)) { if (!validatePWRFile(dest, expectedSize)) {
console.log(`[PWR Validation] PWR file validation failed, deleting corrupted file: ${dest}`); console.log(`[PWR Validation] PWR file validation failed, deleting corrupted file: ${dest}`);
fs.unlinkSync(dest); fs.unlinkSync(dest);
throw new Error('Downloaded PWR file is corrupted or invalid. Please retry'); throw new Error('Downloaded PWR file is corrupted or invalid. Please retry');
@@ -188,7 +228,7 @@ async function retryPWRDownload(branch, fileName, progressCallback, cacheDir = C
return await downloadPWR(branch, fileName, progressCallback, cacheDir, true); return await downloadPWR(branch, fileName, progressCallback, cacheDir, true);
} }
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, branch = 'release', cacheDir = CACHE_DIR) { async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, branch = 'release', cacheDir = CACHE_DIR, skipExistingCheck = false) {
console.log(`[Butler] Starting PWR application with:`); console.log(`[Butler] Starting PWR application with:`);
console.log(`[Butler] - PWR file: ${pwrFile}`); console.log(`[Butler] - PWR file: ${pwrFile}`);
console.log(`[Butler] - Staging dir: ${path.join(gameDir, 'staging-temp')}`); console.log(`[Butler] - Staging dir: ${path.join(gameDir, 'staging-temp')}`);
@@ -212,11 +252,12 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
const gameLatest = gameDir; const gameLatest = gameDir;
const stagingDir = path.join(gameLatest, 'staging-temp'); const stagingDir = path.join(gameLatest, 'staging-temp');
const clientPath = findClientPath(gameLatest); if (!skipExistingCheck) {
const clientPath = findClientPath(gameLatest);
if (clientPath) { if (clientPath) {
console.log('Game files detected, skipping patch installation.'); console.log('Game files detected, skipping patch installation.');
return; return;
}
} }
// Validate and prepare directories // Validate and prepare directories
@@ -397,57 +438,118 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
} }
console.log(`Updating game files to version: ${newVersion} (branch: ${branch})`); console.log(`Updating game files to version: ${newVersion} (branch: ${branch})`);
tempUpdateDir = path.join(gameDir, '..', 'temp_update'); // Determine update strategy: intermediate patches vs full reinstall
const currentVersion = loadVersionClient();
const currentBuild = extractVersionNumber(currentVersion) || 0;
const targetBuild = extractVersionNumber(newVersion);
if (fs.existsSync(tempUpdateDir)) { let useIntermediatePatches = false;
fs.rmSync(tempUpdateDir, { recursive: true, force: true }); let updatePlan = null;
}
fs.mkdirSync(tempUpdateDir, { recursive: true });
if (progressCallback) { if (currentBuild > 0 && currentBuild < targetBuild) {
progressCallback('Downloading new game version...', 20, null, null, null); try {
} updatePlan = await getUpdatePlan(currentBuild, targetBuild, branch);
useIntermediatePatches = !updatePlan.isFullInstall;
const pwrFile = await downloadPWR(branch, newVersion, progressCallback, cacheDir); if (useIntermediatePatches) {
const totalMB = (updatePlan.totalSize / 1024 / 1024).toFixed(0);
if (progressCallback) { console.log(`[UpdateGameFiles] Using intermediate patches: ${updatePlan.steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')} (${totalMB} MB)`);
progressCallback('Extracting new files...', 60, null, null, null); }
} } catch (planError) {
console.warn('[UpdateGameFiles] Could not get update plan, falling back to full install:', planError.message);
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir);
// Delete PWR file from cache after successful update
try {
if (fs.existsSync(pwrFile)) {
fs.unlinkSync(pwrFile);
console.log('[UpdateGameFiles] PWR file deleted from cache after successful update:', pwrFile);
} }
} catch (delErr) {
console.warn('[UpdateGameFiles] Failed to delete PWR file from cache:', delErr.message);
}
if (progressCallback) {
progressCallback('Replacing game files...', 80, null, null, null);
} }
if (fs.existsSync(gameDir)) { if (useIntermediatePatches && updatePlan) {
console.log('Removing old game files...'); // Apply intermediate patches directly to game dir
let retries = 3; for (let i = 0; i < updatePlan.steps.length; i++) {
while (retries > 0) { const step = updatePlan.steps[i];
const stepName = `${step.from}_to_${step.to}`;
if (progressCallback) {
const progress = 20 + Math.round((i / updatePlan.steps.length) * 60);
progressCallback(`Downloading patch ${i + 1}/${updatePlan.steps.length} (${stepName})...`, progress, null, null, null);
}
const pwrFile = await downloadPWR(branch, stepName, progressCallback, cacheDir, false, step.url, step.size);
if (!pwrFile) {
throw new Error(`Failed to download patch ${stepName}`);
}
if (progressCallback) {
progressCallback(`Applying patch ${i + 1}/${updatePlan.steps.length} (${stepName})...`, null, null, null, null);
}
await applyPWR(pwrFile, progressCallback, gameDir, toolsDir, branch, cacheDir, true);
// Clean up PWR file from cache
try { try {
fs.rmSync(gameDir, { recursive: true, force: true }); if (fs.existsSync(pwrFile)) {
break; fs.unlinkSync(pwrFile);
} catch (err) { }
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) { } catch (delErr) {
retries--; console.warn('[UpdateGameFiles] Failed to delete PWR from cache:', delErr.message);
console.log(`[UpdateGameFiles] Removal failed with ${err.code}, retrying in 1s... (${retries} retries left)`); }
await new Promise(resolve => setTimeout(resolve, 1000));
} else { // Save intermediate version so we can resume if interrupted
throw err; saveVersionClient(`v${step.to}`);
console.log(`[UpdateGameFiles] Applied patch ${stepName} (${i + 1}/${updatePlan.steps.length})`);
}
} else {
// Full install: download 0->target, apply to temp dir, swap
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...', 20, null, null, null);
}
const pwrFile = await downloadPWR(branch, newVersion, progressCallback, cacheDir);
if (progressCallback) {
progressCallback('Extracting new files...', 60, null, null, null);
}
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir);
try {
if (fs.existsSync(pwrFile)) {
fs.unlinkSync(pwrFile);
console.log('[UpdateGameFiles] PWR file deleted from cache after successful update:', pwrFile);
}
} catch (delErr) {
console.warn('[UpdateGameFiles] Failed to delete PWR file from cache:', delErr.message);
}
if (progressCallback) {
progressCallback('Replacing game files...', 80, null, null, null);
}
if (fs.existsSync(gameDir)) {
console.log('Removing old game files...');
let retries = 3;
while (retries > 0) {
try {
fs.rmSync(gameDir, { recursive: true, force: true });
break;
} catch (err) {
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
retries--;
console.log(`[UpdateGameFiles] Removal failed with ${err.code}, retrying in 1s... (${retries} retries left)`);
await new Promise(resolve => setTimeout(resolve, 1000));
} else {
throw err;
}
} }
} }
} }
}
fs.renameSync(tempUpdateDir, gameDir); fs.renameSync(tempUpdateDir, gameDir);
}
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback); const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
console.log('HomePage.ui update result after update:', homeUIResult); console.log('HomePage.ui update result after update:', homeUIResult);
@@ -818,7 +920,8 @@ function validateGameDirectory(gameDir, stagingDir) {
} }
// Enhanced PWR file validation // Enhanced PWR file validation
function validatePWRFile(filePath) { // Accepts intermediate patches (50+ MB) and full installs (1.5+ GB)
function validatePWRFile(filePath, expectedSize = null) {
try { try {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
return false; return false;
@@ -827,27 +930,20 @@ function validatePWRFile(filePath) {
const stats = fs.statSync(filePath); const stats = fs.statSync(filePath);
const sizeInMB = stats.size / 1024 / 1024; const sizeInMB = stats.size / 1024 / 1024;
// PWR files should be at least 1 MB
if (stats.size < 1024 * 1024) { if (stats.size < 1024 * 1024) {
console.log(`[PWR Validation] File too small: ${sizeInMB.toFixed(2)} MB`);
return false; return false;
} }
// Check if file is under 1.5 GB (incomplete download) // Validate against expected size if known (reject if < 99% of expected)
if (sizeInMB < 1500) { if (expectedSize && stats.size < expectedSize * 0.99) {
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`); const expectedMB = expectedSize / 1024 / 1024;
console.log(`[PWR Validation] File truncated: ${sizeInMB.toFixed(2)} MB, expected ${expectedMB.toFixed(2)} MB`);
return false; return false;
} }
// Basic file header validation (PWR files should have specific headers) console.log(`[PWR Validation] File size: ${sizeInMB.toFixed(2)} MB - OK`);
const buffer = fs.readFileSync(filePath, { start: 0, end: 20 });
if (buffer.length < 10) {
return false;
}
// Check for common PWR magic bytes or patterns
// This is a basic check - could be enhanced with actual PWR format specification
const header = buffer.toString('hex', 0, 10);
console.log(`[PWR Validation] File header: ${header}`);
return true; return true;
} catch (error) { } catch (error) {
console.error(`[PWR Validation] Error:`, error.message); console.error(`[PWR Validation] Error:`, error.message);

View File

@@ -2,40 +2,248 @@ const axios = require('axios');
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs'); const fs = require('fs');
const { getOS, getArch } = require('../utils/platformUtils'); const { getOS, getArch } = require('../utils/platformUtils');
const { smartRequest } = require('../utils/proxyClient');
const BASE_PATCH_URL = 'https://game-patches.hytale.com/patches'; // Patches CDN via auth server redirect gateway (allows instant CDN switching)
const MANIFEST_API = 'https://files.hytalef2p.com/api/patch_manifest'; const AUTH_DOMAIN = process.env.HYTALE_AUTH_DOMAIN || 'auth.sanasol.ws';
const MIRROR_BASE_URL = `https://${AUTH_DOMAIN}/patches`;
const MIRROR_MANIFEST_URL = `${MIRROR_BASE_URL}/manifest.json`;
// Fallback: latest known build number if manifest is unreachable
const FALLBACK_LATEST_BUILD = 11;
let manifestCache = null;
let manifestCacheTime = 0;
const MANIFEST_CACHE_DURATION = 60000; // 1 minute
/**
* Fetch the mirror manifest from MEGA S4
*/
async function fetchMirrorManifest() {
const now = Date.now();
if (manifestCache && (now - manifestCacheTime) < MANIFEST_CACHE_DURATION) {
console.log('[Mirror] Using cached manifest');
return manifestCache;
}
try {
console.log('[Mirror] Fetching manifest from:', MIRROR_MANIFEST_URL);
const response = await axios.get(MIRROR_MANIFEST_URL, {
timeout: 15000,
headers: { 'User-Agent': 'Hytale-F2P-Launcher' }
});
if (response.data && response.data.files) {
manifestCache = response.data;
manifestCacheTime = now;
console.log('[Mirror] Manifest fetched successfully');
return response.data;
}
throw new Error('Invalid manifest structure');
} catch (error) {
console.error('[Mirror] Error fetching manifest:', error.message);
if (manifestCache) {
console.log('[Mirror] Using expired cache');
return manifestCache;
}
throw error;
}
}
/**
* Parse manifest to get available patches for current platform
* Returns array of { from, to, key, size }
*/
function getPlatformPatches(manifest, branch = 'release') {
const os = getOS();
const arch = getArch();
const prefix = `${os}/${arch}/${branch}/`;
const patches = [];
for (const [key, info] of Object.entries(manifest.files)) {
if (key.startsWith(prefix) && key.endsWith('.pwr')) {
const filename = key.slice(prefix.length, -4); // e.g., "0_to_11"
const match = filename.match(/^(\d+)_to_(\d+)$/);
if (match) {
patches.push({
from: parseInt(match[1]),
to: parseInt(match[2]),
key,
size: info.size
});
}
}
}
return patches;
}
/**
* Find optimal patch path using BFS with download size minimization
* Returns array of { from, to, url, size, key } steps, or null if no path found
*/
function findOptimalPatchPath(currentBuild, targetBuild, patches) {
if (currentBuild >= targetBuild) return [];
const edges = {};
for (const patch of patches) {
if (!edges[patch.from]) edges[patch.from] = [];
edges[patch.from].push(patch);
}
const queue = [{ build: currentBuild, path: [], totalSize: 0 }];
let bestPath = null;
let bestSize = Infinity;
while (queue.length > 0) {
const { build, path, totalSize } = queue.shift();
if (build === targetBuild) {
if (totalSize < bestSize) {
bestPath = path;
bestSize = totalSize;
}
continue;
}
if (totalSize >= bestSize) continue;
const nextEdges = edges[build] || [];
for (const edge of nextEdges) {
if (edge.to <= build || edge.to > targetBuild) continue;
if (path.some(p => p.to === edge.to)) continue;
queue.push({
build: edge.to,
path: [...path, {
from: edge.from,
to: edge.to,
url: `${MIRROR_BASE_URL}/${edge.key}`,
size: edge.size,
key: edge.key
}],
totalSize: totalSize + edge.size
});
}
}
return bestPath;
}
/**
* Get the optimal update plan from currentBuild to targetBuild
* Returns { steps: [{from, to, url, size}], totalSize, isFullInstall }
*/
async function getUpdatePlan(currentBuild, targetBuild, branch = 'release') {
const manifest = await fetchMirrorManifest();
const patches = getPlatformPatches(manifest, branch);
// Try optimal path
const steps = findOptimalPatchPath(currentBuild, targetBuild, patches);
if (steps && steps.length > 0) {
const totalSize = steps.reduce((sum, s) => sum + s.size, 0);
console.log(`[Mirror] Update plan: ${steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')} (${(totalSize / 1024 / 1024).toFixed(0)} MB)`);
return { steps, totalSize, isFullInstall: steps.length === 1 && steps[0].from === 0 };
}
// Fallback: full install 0 -> target
const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
if (fullPatch) {
const step = {
from: 0,
to: targetBuild,
url: `${MIRROR_BASE_URL}/${fullPatch.key}`,
size: fullPatch.size,
key: fullPatch.key
};
console.log(`[Mirror] Full install: 0\u2192${targetBuild} (${(fullPatch.size / 1024 / 1024).toFixed(0)} MB)`);
return { steps: [step], totalSize: fullPatch.size, isFullInstall: true };
}
throw new Error(`No patch path found from build ${currentBuild} to ${targetBuild} for ${getOS()}/${getArch()}`);
}
async function getLatestClientVersion(branch = 'release') { async function getLatestClientVersion(branch = 'release') {
try { try {
console.log(`Fetching latest client version from API (branch: ${branch})...`); console.log(`[Mirror] Fetching latest client version (branch: ${branch})...`);
const response = await smartRequest(`https://files.hytalef2p.com/api/version_client?branch=${branch}`, { const manifest = await fetchMirrorManifest();
timeout: 40000, const patches = getPlatformPatches(manifest, branch);
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
if (response.data && response.data.client_version) { if (patches.length === 0) {
const version = response.data.client_version; console.log(`[Mirror] No patches for branch '${branch}', using fallback`);
console.log(`Latest client version for ${branch}: ${version}`); return `v${FALLBACK_LATEST_BUILD}`;
return version; }
const latestBuild = Math.max(...patches.map(p => p.to));
console.log(`[Mirror] Latest client version: v${latestBuild}`);
return `v${latestBuild}`;
} catch (error) {
console.error('[Mirror] Error:', error.message);
return `v${FALLBACK_LATEST_BUILD}`;
}
}
/**
* Get PWR download URL for fresh install (0 -> target)
* Backward-compatible with old getPWRUrlFromNewAPI signature
* Checks mirror first, then constructs URL for the branch
*/
async function getPWRUrl(branch = 'release', version = 'v11') {
const targetBuild = extractVersionNumber(version);
const os = getOS();
const arch = getArch();
try {
const manifest = await fetchMirrorManifest();
const patches = getPlatformPatches(manifest, branch);
const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
if (fullPatch) {
const url = `${MIRROR_BASE_URL}/${fullPatch.key}`;
console.log(`[Mirror] PWR URL: ${url}`);
return url;
}
if (patches.length > 0) {
// Branch exists in mirror but no full patch for this target - construct URL
console.log(`[Mirror] No 0->${targetBuild} patch found, constructing URL`);
} else { } else {
console.log('Warning: Invalid API response, falling back to latest known version (7.pwr)'); console.log(`[Mirror] Branch '${branch}' not in mirror, constructing URL`);
return '7.pwr';
} }
} catch (error) { } catch (error) {
console.error('Error fetching client version:', error.message); console.error('[Mirror] Error getting PWR URL:', error.message);
console.log('Warning: API unavailable, falling back to latest known version (7.pwr)');
return '7.pwr';
} }
// Construct mirror URL (will work if patch was uploaded but manifest is stale)
return `${MIRROR_BASE_URL}/${os}/${arch}/${branch}/0_to_${targetBuild}.pwr`;
}
// Backward-compatible alias
const getPWRUrlFromNewAPI = getPWRUrl;
// Utility function to extract version number
// Supports: "7.pwr", "v8", "v8-windows-amd64.pwr", "5_to_10", etc.
function extractVersionNumber(version) {
if (!version) return 0;
// New format: "v8" or "v8-xxx.pwr"
const vMatch = version.match(/v(\d+)/);
if (vMatch) return parseInt(vMatch[1]);
// Old format: "7.pwr"
const pwrMatch = version.match(/(\d+)\.pwr/);
if (pwrMatch) return parseInt(pwrMatch[1]);
// Fallback
const num = parseInt(version);
return isNaN(num) ? 0 : num;
} }
function buildArchiveUrl(buildNumber, branch = 'release') { function buildArchiveUrl(buildNumber, branch = 'release') {
const os = getOS(); const os = getOS();
const arch = getArch(); const arch = getArch();
return `${BASE_PATCH_URL}/${os}/${arch}/${branch}/0/${buildNumber}.pwr`; return `${MIRROR_BASE_URL}/${os}/${arch}/${branch}/0_to_${buildNumber}.pwr`;
} }
async function checkArchiveExists(buildNumber, branch = 'release') { async function checkArchiveExists(buildNumber, branch = 'release') {
@@ -43,91 +251,56 @@ async function checkArchiveExists(buildNumber, branch = 'release') {
try { try {
const response = await axios.head(url, { timeout: 10000 }); const response = await axios.head(url, { timeout: 10000 });
return response.status === 200; return response.status === 200;
} catch (error) { } catch {
return false; return false;
} }
} }
async function discoverAvailableVersions(latestKnown, branch = 'release', maxProbe = 50) { async function discoverAvailableVersions(latestKnown, branch = 'release') {
const available = [];
const latest = parseInt(latestKnown.replace('.pwr', ''));
for (let i = latest; i >= Math.max(1, latest - maxProbe); i--) {
const exists = await checkArchiveExists(i, branch);
if (exists) {
available.push(`${i}.pwr`);
}
}
return available;
}
async function fetchPatchManifest(branch = 'release') {
try { try {
const os = getOS(); const manifest = await fetchMirrorManifest();
const arch = getArch(); const patches = getPlatformPatches(manifest, branch);
const response = await smartRequest(`${MANIFEST_API}?branch=${branch}&os=${os}&arch=${arch}`, { const versions = [...new Set(patches.map(p => p.to))].sort((a, b) => b - a);
timeout: 10000 return versions.map(v => `${v}.pwr`);
}); } catch {
return response.data.patches || {}; return [];
} catch (error) {
console.error('Failed to fetch patch manifest:', error.message);
return {};
} }
} }
async function extractVersionDetails(targetVersion, branch = 'release') { async function extractVersionDetails(targetVersion, branch = 'release') {
const buildNumber = parseInt(targetVersion.replace('.pwr', '')); const buildNumber = extractVersionNumber(targetVersion);
const previousBuild = buildNumber - 1; const fullUrl = buildArchiveUrl(buildNumber, branch);
const manifest = await fetchPatchManifest(branch);
const patchInfo = manifest[buildNumber];
return { return {
version: targetVersion, version: targetVersion,
buildNumber: buildNumber, buildNumber,
buildName: `HYTALE-Build-${buildNumber}`, buildName: `HYTALE-Build-${buildNumber}`,
fullUrl: patchInfo?.original_url || buildArchiveUrl(buildNumber, branch), fullUrl,
differentialUrl: patchInfo?.patch_url || null, differentialUrl: null,
checksum: patchInfo?.patch_hash || null, checksum: null,
sourceVersion: patchInfo?.from ? `${patchInfo.from}.pwr` : (previousBuild > 0 ? `${previousBuild}.pwr` : null), sourceVersion: null,
isDifferential: !!patchInfo?.proper_patch, isDifferential: false,
releaseNotes: patchInfo?.patch_note || null releaseNotes: null
}; };
} }
function canUseDifferentialUpdate(currentVersion, targetDetails) { function canUseDifferentialUpdate() {
if (!targetDetails) return false; // Differential updates are now handled via getUpdatePlan()
if (!targetDetails.differentialUrl) return false; return false;
if (!targetDetails.isDifferential) return false;
if (!currentVersion) return false;
const currentBuild = parseInt(currentVersion.replace('.pwr', ''));
const expectedSource = parseInt(targetDetails.sourceVersion?.replace('.pwr', '') || '0');
return currentBuild === expectedSource;
} }
function needsIntermediatePatches(currentVersion, targetVersion) { function needsIntermediatePatches(currentVersion, targetVersion) {
if (!currentVersion) return []; if (!currentVersion) return [];
const current = extractVersionNumber(currentVersion);
const current = parseInt(currentVersion.replace('.pwr', '')); const target = extractVersionNumber(targetVersion);
const target = parseInt(targetVersion.replace('.pwr', '')); if (current >= target) return [];
return [targetVersion];
const intermediates = [];
for (let i = current + 1; i <= target; i++) {
intermediates.push(`${i}.pwr`);
}
return intermediates;
} }
async function computeFileChecksum(filePath) { async function computeFileChecksum(filePath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const hash = crypto.createHash('sha256'); const hash = crypto.createHash('sha256');
const stream = fs.createReadStream(filePath); const stream = fs.createReadStream(filePath);
stream.on('data', data => hash.update(data)); stream.on('data', data => hash.update(data));
stream.on('end', () => resolve(hash.digest('hex'))); stream.on('end', () => resolve(hash.digest('hex')));
stream.on('error', reject); stream.on('error', reject);
@@ -136,7 +309,6 @@ async function computeFileChecksum(filePath) {
async function validateChecksum(filePath, expectedChecksum) { async function validateChecksum(filePath, expectedChecksum) {
if (!expectedChecksum) return true; if (!expectedChecksum) return true;
const actualChecksum = await computeFileChecksum(filePath); const actualChecksum = await computeFileChecksum(filePath);
return actualChecksum === expectedChecksum; return actualChecksum === expectedChecksum;
} }
@@ -145,7 +317,7 @@ function getInstalledClientVersion() {
try { try {
const { loadVersionClient } = require('../core/config'); const { loadVersionClient } = require('../core/config');
return loadVersionClient(); return loadVersionClient();
} catch (err) { } catch {
return null; return null;
} }
} }
@@ -160,5 +332,13 @@ module.exports = {
needsIntermediatePatches, needsIntermediatePatches,
computeFileChecksum, computeFileChecksum,
validateChecksum, validateChecksum,
getInstalledClientVersion getInstalledClientVersion,
fetchMirrorManifest,
getPWRUrl,
getPWRUrlFromNewAPI,
getUpdatePlan,
extractVersionNumber,
getPlatformPatches,
findOptimalPatchPath,
MIRROR_BASE_URL
}; };

View File

@@ -1,3 +1,2 @@
provider: github provider: generic
owner: amiayweb # Change to your own GitHub username url: https://git.sanhost.net/sanasol/hytale-f2p/releases/download/latest
repo: Hytale-F2P

10
main.js
View File

@@ -89,7 +89,7 @@ function setDiscordActivity() {
}, },
{ {
label: 'Discord', label: 'Discord',
url: 'https://discord.gg/hf2pdc' url: 'https://discord.gg/Fhbb9Yk5WW'
} }
] ]
}); });
@@ -627,7 +627,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
console.log('[Main] Processing Butler error with retry context'); console.log('[Main] Processing Butler error with retry context');
errorData.retryData = { errorData.retryData = {
branch: error.branch || 'release', branch: error.branch || 'release',
fileName: error.fileName || '7.pwr', fileName: error.fileName || 'v8',
cacheDir: error.cacheDir cacheDir: error.cacheDir
}; };
errorData.canRetry = error.canRetry !== false; errorData.canRetry = error.canRetry !== false;
@@ -647,7 +647,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
console.log('[Main] Processing generic error, creating default retry data'); console.log('[Main] Processing generic error, creating default retry data');
errorData.retryData = { errorData.retryData = {
branch: 'release', branch: 'release',
fileName: '7.pwr' fileName: 'v8'
}; };
// For generic errors, assume it's retryable unless specified // For generic errors, assume it's retryable unless specified
errorData.canRetry = error.canRetry !== false; errorData.canRetry = error.canRetry !== false;
@@ -887,7 +887,7 @@ ipcMain.handle('retry-download', async (event, retryData) => {
console.log('[IPC] Invalid retry data, using PWR defaults'); console.log('[IPC] Invalid retry data, using PWR defaults');
retryData = { retryData = {
branch: 'release', branch: 'release',
fileName: '7.pwr' fileName: 'v8'
}; };
} }
@@ -921,7 +921,7 @@ ipcMain.handle('retry-download', async (event, retryData) => {
} : } :
{ {
branch: retryData?.branch || 'release', branch: retryData?.branch || 'release',
fileName: retryData?.fileName || '7.pwr', fileName: retryData?.fileName || 'v8',
cacheDir: retryData?.cacheDir cacheDir: retryData?.cacheDir
}; };

View File

@@ -1,6 +1,6 @@
{ {
"name": "hytale-f2p-launcher", "name": "hytale-f2p-launcher",
"version": "2.2.1", "version": "2.3.2",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support", "description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
"homepage": "https://github.com/amiayweb/Hytale-F2P", "homepage": "https://github.com/amiayweb/Hytale-F2P",
"main": "main.js", "main": "main.js",
@@ -118,9 +118,8 @@
"createStartMenuShortcut": true "createStartMenuShortcut": true
}, },
"publish": { "publish": {
"provider": "github", "provider": "generic",
"owner": "amiayweb", "url": "https://git.sanhost.net/sanasol/hytale-f2p/releases/download/latest"
"repo": "Hytale-F2P"
} }
} }
} }