Compare commits

..

170 Commits

Author SHA1 Message Date
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
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
sanasol
98123d7338 feat(macos): add code signing and notarization support
Add macOS code signing and notarization for Gatekeeper compatibility:

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

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 10:55:00 +01:00
Fazri Gading
6761f6b3e0 chore: fix release notes for v2.2.1 2026-02-02 08:57:37 +08:00
Fazri Gading
b1aeb9fe4a chore: improvise badges, relocate star history, fix discord links 2026-02-02 08:49:09 +08:00
Fazri Gading
c27e1f4cd4 chore revise windows prerequisites and changelog
Updated prerequisites and changelog for version 2.2.1.
2026-02-02 08:30:06 +08:00
Fazri Gading
f0939a60c9 docs: enhance bug report template with placeholders and options
Updated the bug report template to include placeholders and additional Linux distributions.
2026-02-02 08:06:30 +08:00
Fazri Gading
23fad047c0 Prepare Release v2.2.1 2026-02-02 06:22:10 +08:00
Fazri Gading
5f3c9e0411 fix: redact proxy_url and remove timed out emoji 2026-02-02 06:17:35 +08:00
Fazri Gading
2e0bdeee5a 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
2026-02-02 06:09:14 +08:00
Fazri Gading
d8d7702d9d fix: remove duplicate check 2026-02-02 04:31:41 +08:00
AMIAY
3ac2f25955 Add smart proxy with direct-fallback and logging 2026-02-01 19:46:22 +01:00
Fazri Gading
7c8a106f06 Merge pull request #257 from amiayweb/fix/libzstd-reorder
fix: reorder fedora libzstd paths to first iteration
2026-02-02 01:38:27 +08:00
Fazri Gading
0015ecbe80 fix: add game_running_marker to prevent duplicate launches 2026-02-02 01:35:43 +08:00
AMIAY
b5cd9ca791 package version to 2.2.1
Update package.json version from 2.2.0 to 2.2.1 to publish a patch release.
2026-02-01 18:16:48 +01:00
AMIAY
d5da9ecb6d 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
2026-02-01 18:01:58 +01:00
AMIAY
b1d01a2f34 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.
2026-02-01 17:35:25 +01:00
AMIAY
cd25f124bd Add proxy client and route downloads through it 2026-02-01 17:23:00 +01:00
AMIAY
fc91560acb Merge pull request #255 from amiayweb/fix/eperm-in-repair-button
Major Bug Fix in v2.2.0: HytaleClient did not launch in Windows especially Laptop, Ghost Processes on game/launcher exit, and EPERM in Repair Game Button
2026-02-01 16:35:30 +01:00
Fazri Gading
3370628b6e revert add deps for generateLocalTokens 2026-02-01 23:25:13 +08:00
Fazri Gading
3fee5b0f72 revert generateLocalTokens, wrong analysis on game launching issue 2026-02-01 23:24:22 +08:00
Fazri Gading
38d436ceb7 docs: add analysis on ghost process and launcher cleanup 2026-02-01 23:16:26 +08:00
Fazri Gading
6ee23e1944 fix: major bug - detach game process to avoid launcher-held handles causing zombie process 2026-02-01 23:12:29 +08:00
Fazri Gading
5de155f190 fix: discord RPC destroy error if not connected 2026-02-01 23:05:14 +08:00
Fazri Gading
b84457d88d fix: major bug - hytale won't launch with laptop machine and ghost processes 2026-02-01 22:58:20 +08:00
Fazri Gading
1ef96561bf Merge pull request #253 from MetricsLite/patch-3
Fix Turkish translations in tr-TR.json
2026-02-01 21:31:12 +08:00
Fazri Gading
d91ba72969 Merge commit '6847a54c0f25219490324521ec98c326a353e1a8' into fix/eperm-in-repair-button 2026-02-01 21:27:59 +08:00
Fazri Gading
ecaaa28866 fix: invalid generated token that caused hangs on exit [windows testing needed] 2026-02-01 19:26:24 +08:00
Fazri Gading
ad34741627 fix: EPERM error in Repair Game Button [windows testing needed] 2026-02-01 19:24:59 +08:00
MetricsLite
a346e9d9e3 Fix Turkish translations in tr-TR.json 2026-02-01 12:20:41 +03:00
Fazri Gading
1f4e91c975 chore revise online play hosting instructions in README
Updated instructions for hosting an online game and clarified troubleshooting steps.
2026-02-01 15:31:10 +08:00
Fazri Gading
a1a45a2d31 chore: update discord link and give warning quickstart 2026-02-01 15:27:46 +08:00
Fazri Gading
2ed402b14b chore: update quickstart button link to header 2026-02-01 15:22:44 +08:00
Fazri Gading
4ce6fbee0a fix: missing version text on launcher 2026-02-01 15:03:03 +08:00
Fazri Gading
3dfaa1c778 chore: insert contact link in CODE_OF_CONDUCT.md 2026-02-01 05:14:46 +08:00
Fazri Gading
6a4da66a1e chore: update discord link 2026-02-01 05:13:51 +08:00
Fazri Gading
53939fc0ae chore: link downloads, platform, and version to release page in README.md 2026-02-01 05:10:50 +08:00
Fazri Gading
fb135d3486 chore: rearrange, fix, and improve README.md 2026-02-01 05:08:39 +08:00
Alex
62430fe8f0 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>
2026-02-01 05:01:58 +08:00
Fazri Gading
6847a54c0f feat: enhance gpu detection, drafting 2026-02-01 04:11:55 +08:00
Fazri Gading
95d47f0e60 fix: reorder fedora libzstd paths to first iteration 2026-02-01 03:17:20 +08:00
Fazri Gading
6fbf37422f Merge remote-tracking branch 'upstream/main' into develop 2026-02-01 03:13:55 +08:00
Fazri Gading
7b2acd49b6 chore: sync package-lock with package.json 2026-01-31 22:58:02 +08:00
Fazri Gading
7b9951e72d Update release.yml 2026-01-31 20:34:57 +08:00
Fazri Gading
e81a0167c1 Merge pull request #246 from amiayweb/fix/v2.2.0-failed-release
Fix/v2.2.0 failed release
2026-01-31 20:32:31 +08:00
Fazri Gading
50c04b64df Merge branch 'develop' into fix/v2.2.0-failed-release 2026-01-31 20:32:15 +08:00
Fazri Gading
28e5fa35e1 update package-lock.json 2026-01-31 20:05:33 +08:00
Fazri Gading
52e7eafe0b fix: redo package.json arch 2026-01-31 19:22:09 +08:00
Fazri Gading
3de5c2eaa3 fix: removed arm64 flags 2026-01-31 19:19:34 +08:00
Fazri Gading
5147e1856f fix: preserves arch x64 on linux target for #242 2026-01-31 18:16:39 +08:00
Fazri Gading
a21e7e4910 chose: add auto-patch system for pre-release JAR 2026-01-31 05:52:20 +08:00
AMIAY
14a63febc1 Support branch selection for server patching 2026-01-30 22:45:21 +01:00
Fazri Gading
2cdef44fec fix: gamescope steam deck issue fixes #186 hopefully 2026-01-31 05:26:43 +08:00
Fazri Gading
f8cf41972d fix: build and release for tag push-only in release.yml 2026-01-31 04:34:58 +08:00
Fazri Gading
ea0f87c46a chore: delete icon.png, moved to build folder 2026-01-31 04:23:55 +08:00
Fazri Gading
a5b3fe02c8 chore: delete icon.ico, moved to build folder 2026-01-31 04:23:33 +08:00
Fazri Gading
0bb82a0b3d Release v2.2.0 2026-01-31 04:22:05 +08:00
Fazri Gading
eccdcf223e Release v2.2.0 2026-01-31 04:15:50 +08:00
Fazri Gading
a09b082152 Release v2.2.0 2026-01-31 04:04:34 +08:00
Fazri Gading
f1d01ac78c docs: add recordings form, fix OS list 2026-01-31 02:56:37 +08:00
AMIAY
bfe0156606 Add Discord invite link to rpc 2026-01-30 19:02:12 +01:00
Fazri Gading
78e97bdbb7 feat: create two columns for settings page 2026-01-31 01:42:20 +08:00
Fazri Gading
769bc2054c fix: GPU preference hint to Laptop-only 2026-01-31 00:52:02 +08:00
Fazri Gading
5337441d97 feat: add Indonesian language translation 2026-01-31 00:51:24 +08:00
Fazri Gading
12453d2dda fix: src.tar.zst and srcinfo missing files 2026-01-30 23:50:54 +08:00
Fazri Gading
803df90fb6 fix: pkgbuild version to 2.1.2 2026-01-30 23:50:29 +08:00
Fazri Gading
6c31c39abd fix: removed override tar version 2026-01-30 23:23:13 +08:00
Fazri Gading
b5ab8b78e8 fix: upgrade electron/rebuild to 4.0.3 2026-01-30 22:50:14 +08:00
Fazri Gading
343f7b8016 Merge branch 'develop' 2026-01-30 22:39:43 +08:00
Fazri Gading
fa568fcce7 fix: re-add universal arch for mac 2026-01-30 22:39:26 +08:00
Fazri Gading
a6ecd2c167 Merge branch 'develop' 2026-01-30 22:26:50 +08:00
Fazri Gading
3e1c4aef73 fix: upgrade tar to ^7.5.6 version 2026-01-30 22:24:56 +08:00
Fazri Gading
1c14c3f603 fix: removed 'check disk space' alert on permission file error 2026-01-30 22:13:01 +08:00
AMIAY
30a4327655 Remove launcher chat and add Discord popup 2026-01-30 14:44:46 +01:00
AMIAY
33a0e219fc Add differential update system 2026-01-30 04:11:10 +01:00
AMIAY
fbdd9ee0cf Update Discord invite URL in client patcher 2026-01-30 02:28:45 +01:00
AMIAY
22ea2f56d3 Add Featured Servers page to GUI 2026-01-29 19:00:13 +01:00
AMIAY
5039bcdadf added featured server list from api 2026-01-29 17:07:29 +01:00
Fazri Gading
4db8016a28 chore: delete warning of Ubuntu-Debian at Linux Prequisites section 2026-01-29 23:15:54 +08:00
Fazri Gading
e0fd7e6900 chore: update quickstart link in README.md 2026-01-29 23:14:22 +08:00
AMIAY
93a2a98028 Update installation subtitle 2026-01-29 03:38:46 +01:00
AMIAY
4775e9adbd Enforce 16-char player name limit and update mod sync
Added a maxlength attribute to the player name input and enforced a 16-character limit in both install and settings scripts, providing user feedback if exceeded. Refactored modManager.js to replace symlink-based mod management with a copy-based system, copying enabled mods to HytaleSaves\Mods and removing legacy symlink logic to improve compatibility and avoid permission issues.
2026-01-29 03:33:56 +01:00
AMIAY
90db069e4c delete cache after installation 2026-01-29 00:58:47 +01:00
Terromur
baa585d6b3 Fix PKGBUILD 2026-01-29 04:49:02 +05:00
Terromur
a5b930a9f0 Fix PKGBUILD-git 2026-01-29 04:45:44 +05:00
xSamiVS
b708f4a7d7 Standardize language codes, improve formatting, and update all locale files. (#224)
* Update German (Germany) localization

* Update Español (España) localization

* Update French (France) localization

* Update Polish (Poland) localization

* Update Portuguese (Brazil) localization

* Update Russian (Russia) localization

* Update Swedish (Sweden) localization

* Update Turkish (Turkey) localization

* Update language codes, names and alphabetical in i18n system

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

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

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 03:24:39 +08:00
Fazri Gading
966de83ead fix: change version release to 7.pwr 2026-01-29 03:23:19 +08:00
Fazri Gading
bc7f46cf45 fix: change default release version to 7.pwr 2026-01-29 03:22:30 +08:00
Fazri Gading
534b3f1f34 fix: change default version to 7.pwr in main.js 2026-01-29 03:19:17 +08:00
Fazri Gading
a07f0f1de1 fix: timeout getLatestClient
fixes #138
2026-01-29 03:01:38 +08:00
Terromur
bf29112848 Merge pull request #218 from BlackSystemCoder/develop
Add Russian language support
2026-01-28 22:01:35 +05:00
Zakhar Smokotov
0e4e332dab Update ru.json 2026-01-28 19:53:46 +03:00
Zakhar Smokotov
779f6820cb Update ru.json 2026-01-28 19:49:37 +03:00
Zakhar Smokotov
4fc4d77415 Update ru.json 2026-01-28 19:47:52 +03:00
Zakhar Smokotov
de193e991f Update ru.json 2026-01-28 19:46:30 +03:00
Zakhar Smokotov
d69695e499 Update ru.json 2026-01-28 19:45:29 +03:00
Zakhar Smokotov
4fff87f221 fixed untranslated place 2026-01-28 19:40:39 +03:00
Zakhar Smokotov
4cd76bb96d Fixed Java runtime name and fixed typo 2026-01-28 19:39:41 +03:00
Zakhar Smokotov
721d287036 Update ru.json 2026-01-28 19:33:36 +03:00
Zakhar Smokotov
e491bf1a84 fix 2026-01-28 19:17:37 +03:00
Zakhar Smokotov
89f981b586 Some updates in Russian language localization file 2026-01-28 19:16:19 +03:00
Fazri Gading
9cf504bbcc chore: drafting documentation on SERVER.md 2026-01-28 23:41:27 +08:00
Zakhar Smokotov
e7110936d8 Add Russian language support
Added Russian (ru) to the list of available languages.
2026-01-28 16:27:48 +03:00
AMIAY
79456e43a6 Merge pull request #213 from amiayweb/fix/update-system-improvements 2026-01-28 03:14:05 +01:00
sanasol
dd2dbc6f08 fix: improve update system UX and macOS compatibility
Update System Improvements:
- Fix duplicate update popups by disabling legacy updater.js
- Add skip button to update popup (shows after 30s, on error, or after download)
- Add macOS-specific handling with manual download as primary option
- Add missing open-download-page IPC handler
- Add missing unblockInterface() method to properly clean up after popup close
- Add quitAndInstallUpdate alias in preload for compatibility
- Remove pulse animation when download completes
- Fix manual download button to show correct status and close popup
- Sync player name to settings input after first install

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

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

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

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

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 19:40:42 +01:00
Fazri Gading
ee18455b4b chore: add downloads counter in README.md 2026-01-27 21:14:53 +08:00
Fazri Gading
a5c931b26d chore: add offline-mode warning to the README.md 2026-01-27 18:45:55 +08:00
Fazri Gading
661a0c9eed Update README.md 2026-01-27 17:38:33 +08:00
AMIAY
9025800820 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.
2026-01-27 04:29:01 +01:00
AMIAY
34ee099ae2 french translate 2026-01-27 03:26:43 +01:00
AMIAY
e56b12cd72 userdata migration [need review from other OS] 2026-01-27 01:44:58 +01:00
Fazri Gading
6bd63f5b60 Merge branch 'amiayweb:main' into main 2026-01-27 04:14:19 +08:00
Fazri Gading
663ac5f834 Merge branch 'develop' fix PKGBUILD pkgname variable 2026-01-27 03:58:14 +08:00
Fazri Gading
3edee4b4eb fix: PKGBUILD pkgname variable fix 2026-01-27 03:55:01 +08:00
Fazri Gading
e5fec7c326 Merge branch 'main' into develop 2026-01-27 03:42:40 +08:00
Fazri Gading
7d2672b684 add hardware spec input in bug_report.yml 2026-01-27 03:41:26 +08:00
Fazri Gading
01823729ec fix screenshot input in feature_request.yml 2026-01-27 03:40:22 +08:00
Fazri Gading
639a2ab1b5 chore: add changelog in README.md 2026-01-27 03:38:20 +08:00
Fazri Gading
6b76eb365e Update bug_report.yml
Add logs textfield to bug report
2026-01-27 03:21:47 +08:00
Fazri Gading
6fa933fece Update support_request.yml
Added hardware specification
2026-01-27 03:19:06 +08:00
walti0
e7023dcf95 Polish language support (#195) 2026-01-27 03:06:16 +08:00
Fazri Gading
f4d966ee65 chore: fix ubuntu/debian part in README.md 2026-01-27 02:16:01 +08:00
Fazri Gading
ca835a868b Merge pull request #188 from TalesAmaral/patch-1
Update README.md
2026-01-27 00:19:05 +08:00
Fazri Gading
3a1b6039d0 Merge branch 'develop' into patch-1 2026-01-27 00:18:33 +08:00
Fazri Gading
7828454631 Update PKGBUILD-git 2026-01-27 00:15:25 +08:00
Fazri Gading
cc1c6c334c Update PKGBUILD 2026-01-27 00:14:53 +08:00
TalesAmaral
081ac926e3 Update README.md
BUILD.md location was changed and now this link is poiting to nothing
2026-01-26 11:49:39 -03:00
Fazri Gading
75a450c9ec Update README.md
adds information for Arch build
2026-01-26 18:54:53 +08:00
Fazri Gading
e426690632 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.
2026-01-26 18:33:07 +08:00
Fazri Gading
78f76afe0a 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.
2026-01-26 18:20:37 +08:00
Fazri Gading
131de1dcd7 fix: removes pacman build as it replaced by tar.zst and adds build:arch shortcut for pkgbuild 2026-01-26 17:56:44 +08:00
Fazri Gading
b39877f561 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.
2026-01-26 17:46:40 +08:00
Fazri Gading
0b1b448cce Merge branch 'main' into develop 2026-01-26 13:56:33 +08:00
Fazri Gading
aed00cd067 add arch package .pkg.tar.zst for release 2026-01-26 13:52:18 +08:00
Fazri Gading
eff6fcd520 fix: isbrokenlink should be true to remove the symlink 2026-01-26 12:24:24 +08:00
Fazri Gading
94d4586b97 fix: add pathexists for paths.js to check symlink 2026-01-26 12:09:48 +08:00
Fazri Gading
20faf36b37 fix: remove broken symlink after detected 2026-01-26 12:01:46 +08:00
Fazri Gading
375b422c73 Update README.md Windows Prequisites for ARM64 builds 2026-01-26 11:33:00 +08:00
Fazri Gading
b668bdb45a prepare release 2.1.1 2026-01-26 09:48:26 +08:00
Fazri Gading
653d4429ed prepare release 2.1.1
minor fix EPERM permission error
2026-01-26 09:36:03 +08:00
Fazri Gading
17e15c17f0 prepare release for 2.1.1
minor fix for EPERM error permission
2026-01-26 09:34:16 +08:00
Fazri Gading
b99b22e8bf fix: missing pacman builds 2026-01-26 09:23:15 +08:00
Fazri Gading
9303c17e57 Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop 2026-01-26 08:20:55 +08:00
Fazri Gading
615ee5cadc 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.
2026-01-26 08:19:13 +08:00
9 changed files with 801 additions and 533 deletions

View File

@@ -6,201 +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: Create Virtual .env File
# Because main.js needed physical env, we need to create virtual one to store it
run: |
$env_content = @"
HF2P_PROXY_URL=${{ secrets.HF2P_PROXY_URL }}
HF2P_SECRET_KEY=${{ secrets.HF2P_SECRET_KEY }}
"@
Set-Content -Path .env -Value $env_content
- 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: Create Virtual .env File
run: |
cat << EOF > .env
HF2P_PROXY_URL=${{ secrets.HF2P_PROXY_URL }}
HF2P_SECRET_KEY=${{ secrets.HF2P_SECRET_KEY }}
EOF
- 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: Create Virtual .env File
run: |
cat << EOF > .env
HF2P_PROXY_URL=${{ secrets.HF2P_PROXY_URL }}
HF2P_SECRET_KEY=${{ secrets.HF2P_SECRET_KEY }}
EOF
- 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
cat << EOP > .env
HF2P_PROXY_URL=${{ secrets.HF2P_PROXY_URL }}
HF2P_SECRET_KEY=${{ secrets.HF2P_SECRET_KEY }}
EOP
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
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

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

@@ -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');
@@ -30,7 +30,7 @@ async function acquireGameArchive(downloadUrl, targetPath, checksum, progressCal
} }
console.log(`Downloading game archive from: ${downloadUrl}`); console.log(`Downloading game archive from: ${downloadUrl}`);
try { try {
if (allowRetry) { if (allowRetry) {
await retryDownload(downloadUrl, targetPath, progressCallback); await retryDownload(downloadUrl, targetPath, progressCallback);
@@ -103,13 +103,13 @@ async function deployGameArchive(archivePath, destinationDir, toolsDir, progress
if (error) { if (error) {
const cleanStderr = stderr.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim(); const cleanStderr = stderr.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
const cleanStdout = stdout.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim(); const cleanStdout = stdout.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
if (cleanStderr) console.error('Deployment stderr:', cleanStderr); if (cleanStderr) console.error('Deployment stderr:', cleanStderr);
if (cleanStdout) console.error('Deployment stdout:', cleanStdout); if (cleanStdout) console.error('Deployment stdout:', cleanStdout);
const errorText = (stderr + ' ' + error.message).toLowerCase(); const errorText = (stderr + ' ' + error.message).toLowerCase();
let message = 'Game deployment failed'; let message = 'Game deployment failed';
if (errorText.includes('unexpected eof')) { if (errorText.includes('unexpected eof')) {
message = 'Corrupted archive detected. Please retry download.'; message = 'Corrupted archive detected. Please retry download.';
if (fs.existsSync(archivePath)) { if (fs.existsSync(archivePath)) {
@@ -156,20 +156,20 @@ 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);
} }
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback); await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false); await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
saveVersionClient(targetVersion); saveVersionClient(targetVersion);
@@ -177,16 +177,16 @@ 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);
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false); await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
saveVersionClient(targetVersion); saveVersionClient(targetVersion);
@@ -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);
console.log(`Applying ${plan.steps.length} patch(es): ${plan.steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')}`);
for (let i = 0; i < plan.steps.length; i++) {
const step = plan.steps[i];
const stepName = `${step.from}_to_${step.to}`;
const archivePath = path.join(cacheDir, `${branch}_${stepName}.pwr`);
const isDifferential = step.from !== 0;
for (let i = 0; i < patchesToApply.length; i++) {
const patchVersion = patchesToApply[i];
const versionDetails = await extractVersionDetails(patchVersion, branch);
const canDifferential = canUseDifferentialUpdate(getInstalledClientVersion(), versionDetails);
if (!canDifferential || !versionDetails.differentialUrl) {
console.log(`WARNING: Differential patch not available for ${patchVersion}, using full archive`);
const archiveName = path.basename(versionDetails.fullUrl);
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
if (progressCallback) { 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(patchVersion);
console.log(`Patch ${patchVersion} applied successfully (${i + 1}/${patchesToApply.length})`);
}
console.log(`Update completed successfully. Version ${targetVersion} is now installed.`); saveVersionClient(`v${step.to}`);
console.log(`Patch ${stepName} applied (${i + 1}/${plan.steps.length})`);
}
console.log(`Update completed. Version ${targetVersion} is now installed.`);
} catch (planError) {
console.error('Update plan failed:', planError.message);
console.log('Falling back to full archive download');
// 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 = 'v8', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) { async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback, cacheDir = CACHE_DIR, manualRetry = false, directUrl = null) {
const osName = getOS(); const osName = getOS();
const arch = getArch(); const arch = getArch();
@@ -72,43 +72,38 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
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 { getPWRUrlFromNewAPI } = require('../services/versionManager');
let url; let url;
let isUsingNewAPI = false;
if (directUrl) {
try { url = directUrl;
console.log(`[DownloadPWR] Fetching URL from new API for branch: ${branch}, version: ${fileName}`); console.log(`[DownloadPWR] Using direct URL: ${url}`);
url = await getPWRUrlFromNewAPI(branch, fileName); } else {
isUsingNewAPI = true; const { getPWRUrl } = require('../services/versionManager');
console.log(`[DownloadPWR] Using new API URL: ${url}`); try {
} catch (error) { console.log(`[DownloadPWR] Fetching mirror URL for branch: ${branch}, version: ${fileName}`);
console.error(`[DownloadPWR] Failed to get URL from new API: ${error.message}`); url = await getPWRUrl(branch, fileName);
console.log(`[DownloadPWR] Falling back to old URL format`); console.log(`[DownloadPWR] Mirror URL: ${url}`);
url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${branch}/0/${fileName}.pwr`; } catch (error) {
console.error(`[DownloadPWR] Failed to get mirror URL: ${error.message}`);
const { MIRROR_BASE_URL } = require('../services/versionManager');
url = `${MIRROR_BASE_URL}/${osName}/${arch}/${branch}/0_to_${extractVersionNumber(fileName)}.pwr`;
console.log(`[DownloadPWR] Fallback URL: ${url}`);
}
} }
const dest = path.join(cacheDir, `${branch}_${fileName}.pwr`); const dest = path.join(cacheDir, `${branch}_${fileName}.pwr`);
// Check if file exists and validate it // Check if file exists and validate it
if (fs.existsSync(dest) && !manualRetry) { if (fs.existsSync(dest) && !manualRetry) {
console.log('PWR file found in cache:', dest);
// Validate file size (PWR files should be > 1MB and >= 1.5GB for complete downloads)
const stats = fs.statSync(dest); const stats = fs.statSync(dest);
if (stats.size < 1024 * 1024) { if (stats.size > 1024 * 1024) {
return false; console.log(`[PWR] Using cached file: ${dest} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
} return dest;
// Check if file is under 1.5 GB (incomplete download)
const sizeInMB = stats.size / 1024 / 1024;
if (sizeInMB < 1500) {
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
return false;
} }
console.log(`[PWR] Cached file too small (${stats.size} bytes), re-downloading`);
} }
console.log(`Fetching PWR patch file from ${isUsingNewAPI ? 'NEW API' : 'old API'}:`, url); console.log(`[DownloadPWR] Downloading from: ${url}`);
try { try {
if (manualRetry) { if (manualRetry) {
@@ -203,7 +198,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')}`);
@@ -227,11 +222,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
@@ -412,57 +408,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);
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);
@@ -833,36 +890,23 @@ function validateGameDirectory(gameDir, stagingDir) {
} }
// Enhanced PWR file validation // Enhanced PWR file validation
// Accepts intermediate patches (50+ MB) and full installs (1.5+ GB)
function validatePWRFile(filePath) { function validatePWRFile(filePath) {
try { try {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
return false; return false;
} }
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) console.log(`[PWR Validation] File size: ${sizeInMB.toFixed(2)} MB - OK`);
if (sizeInMB < 1500) {
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
return false;
}
// Basic file header validation (PWR files should have specific headers)
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,186 +2,240 @@ 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 NEW_API_URL = 'https://thecute.cloud/ShipOfYarn/api.php'; const MIRROR_BASE_URL = `https://${AUTH_DOMAIN}/patches`;
const MIRROR_MANIFEST_URL = `${MIRROR_BASE_URL}/manifest.json`;
let apiCache = null; // Fallback: latest known build number if manifest is unreachable
let apiCacheTime = 0; const FALLBACK_LATEST_BUILD = 11;
const API_CACHE_DURATION = 60000; // 1 minute
async function fetchNewAPI() { 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(); const now = Date.now();
if (apiCache && (now - apiCacheTime) < API_CACHE_DURATION) { if (manifestCache && (now - manifestCacheTime) < MANIFEST_CACHE_DURATION) {
console.log('[NewAPI] Using cached API data'); console.log('[Mirror] Using cached manifest');
return apiCache; return manifestCache;
} }
try { try {
console.log('[NewAPI] Fetching from:', NEW_API_URL); console.log('[Mirror] Fetching manifest from:', MIRROR_MANIFEST_URL);
const response = await axios.get(NEW_API_URL, { const response = await axios.get(MIRROR_MANIFEST_URL, {
timeout: 15000, timeout: 15000,
headers: { headers: { 'User-Agent': 'Hytale-F2P-Launcher' }
'User-Agent': 'Hytale-F2P-Launcher'
}
}); });
if (response.data && response.data.hytale) { if (response.data && response.data.files) {
apiCache = response.data; manifestCache = response.data;
apiCacheTime = now; manifestCacheTime = now;
console.log('[NewAPI] API data fetched and cached successfully'); console.log('[Mirror] Manifest fetched successfully');
return response.data; return response.data;
} else {
throw new Error('Invalid API response structure');
} }
throw new Error('Invalid manifest structure');
} catch (error) { } catch (error) {
console.error('[NewAPI] Error fetching API:', error.message); console.error('[Mirror] Error fetching manifest:', error.message);
if (apiCache) { if (manifestCache) {
console.log('[NewAPI] Using expired cache due to error'); console.log('[Mirror] Using expired cache');
return apiCache; return manifestCache;
} }
throw error; throw error;
} }
} }
async function getLatestVersionFromNewAPI(branch = 'release') { /**
try { * Parse manifest to get available patches for current platform
const apiData = await fetchNewAPI(); * Returns array of { from, to, key, size }
const osName = getOS(); */
const arch = getArch(); function getPlatformPatches(manifest, branch = 'release') {
const os = getOS();
let osKey = osName; const arch = getArch();
if (osName === 'darwin') { const prefix = `${os}/${arch}/${branch}/`;
osKey = 'mac'; 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
});
}
} }
const branchData = apiData.hytale[branch];
if (!branchData || !branchData[osKey]) {
throw new Error(`No data found for branch: ${branch}, OS: ${osKey}`);
}
const osData = branchData[osKey];
const versions = Object.keys(osData).filter(key => key.endsWith('.pwr'));
if (versions.length === 0) {
throw new Error(`No .pwr files found for ${osKey}`);
}
const versionNumbers = versions.map(v => {
const match = v.match(/v(\d+)/);
return match ? parseInt(match[1]) : 0;
});
const latestVersionNumber = Math.max(...versionNumbers);
console.log(`[NewAPI] Latest version number: ${latestVersionNumber} for branch ${branch}`);
return `v${latestVersionNumber}`;
} catch (error) {
console.error('[NewAPI] Error getting latest version:', error.message);
throw error;
} }
return patches;
} }
async function getPWRUrlFromNewAPI(branch = 'release', version = 'v8') { /**
try { * Find optimal patch path using BFS with download size minimization
const apiData = await fetchNewAPI(); * Returns array of { from, to, url, size, key } steps, or null if no path found
const osName = getOS(); */
const arch = getArch(); function findOptimalPatchPath(currentBuild, targetBuild, patches) {
if (currentBuild >= targetBuild) return [];
let osKey = osName;
if (osName === 'darwin') { const edges = {};
osKey = 'mac'; for (const patch of patches) {
} if (!edges[patch.from]) edges[patch.from] = [];
edges[patch.from].push(patch);
let fileName;
if (osName === 'windows') {
fileName = `${version}-windows-amd64.pwr`;
} else if (osName === 'linux') {
fileName = `${version}-linux-amd64.pwr`;
} else if (osName === 'darwin') {
fileName = `${version}-darwin-arm64.pwr`;
}
const branchData = apiData.hytale[branch];
if (!branchData || !branchData[osKey]) {
throw new Error(`No data found for branch: ${branch}, OS: ${osKey}`);
}
const osData = branchData[osKey];
const url = osData[fileName];
if (!url) {
throw new Error(`No URL found for ${fileName}`);
}
console.log(`[NewAPI] URL for ${fileName}: ${url}`);
return url;
} catch (error) {
console.error('[NewAPI] Error getting PWR URL:', error.message);
throw error;
} }
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(`[NewAPI] Fetching latest client version from new API (branch: ${branch})...`); console.log(`[Mirror] Fetching latest client version (branch: ${branch})...`);
const manifest = await fetchMirrorManifest();
// Utiliser la nouvelle API const patches = getPlatformPatches(manifest, branch);
const latestVersion = await getLatestVersionFromNewAPI(branch);
console.log(`[NewAPI] Latest client version for ${branch}: ${latestVersion}`);
return latestVersion;
} catch (error) {
console.error('[NewAPI] Error fetching client version from new API:', error.message);
console.log('[NewAPI] Falling back to old API...');
// Fallback vers l'ancienne API si la nouvelle échoue
try {
const response = await smartRequest(`https://files.hytalef2p.com/api/version_client?branch=${branch}`, {
timeout: 40000,
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} (old API): ${version}`); return `v${FALLBACK_LATEST_BUILD}`;
return version;
} else {
console.log('Warning: Invalid API response, falling back to latest known version (v8)');
return 'v8';
}
} catch (fallbackError) {
console.error('Error fetching client version from old API:', fallbackError.message);
console.log('Warning: Both APIs unavailable, falling back to latest known version (v8)');
return 'v8';
} }
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}`;
} }
} }
// Fonction utilitaire pour extraire le numéro de version /**
// Supporte les formats: "7.pwr", "v8", "v8-windows-amd64.pwr", etc. * 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 {
console.log(`[Mirror] Branch '${branch}' not in mirror, constructing URL`);
}
} catch (error) {
console.error('[Mirror] Error getting PWR URL:', error.message);
}
// 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) { function extractVersionNumber(version) {
if (!version) return 0; if (!version) return 0;
// Nouveau format: "v8" ou "v8-xxx.pwr" // New format: "v8" or "v8-xxx.pwr"
const vMatch = version.match(/v(\d+)/); const vMatch = version.match(/v(\d+)/);
if (vMatch) { if (vMatch) return parseInt(vMatch[1]);
return parseInt(vMatch[1]);
} // Old format: "7.pwr"
// Ancien format: "7.pwr"
const pwrMatch = version.match(/(\d+)\.pwr/); const pwrMatch = version.match(/(\d+)\.pwr/);
if (pwrMatch) { if (pwrMatch) return parseInt(pwrMatch[1]);
return parseInt(pwrMatch[1]);
} // Fallback
// Fallback: essayer de parser directement
const num = parseInt(version); const num = parseInt(version);
return isNaN(num) ? 0 : num; return isNaN(num) ? 0 : num;
} }
@@ -189,7 +243,7 @@ function extractVersionNumber(version) {
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') {
@@ -197,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 = extractVersionNumber(latestKnown);
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 = extractVersionNumber(targetVersion); 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 = extractVersionNumber(currentVersion);
const expectedSource = extractVersionNumber(targetDetails.sourceVersion);
return currentBuild === expectedSource;
} }
function needsIntermediatePatches(currentVersion, targetVersion) { function needsIntermediatePatches(currentVersion, targetVersion) {
if (!currentVersion) return []; if (!currentVersion) return [];
const current = extractVersionNumber(currentVersion); const current = extractVersionNumber(currentVersion);
const target = extractVersionNumber(targetVersion); const target = extractVersionNumber(targetVersion);
if (current >= target) return [];
const intermediates = []; return [targetVersion];
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);
@@ -290,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;
} }
@@ -299,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;
} }
} }
@@ -315,8 +333,12 @@ module.exports = {
computeFileChecksum, computeFileChecksum,
validateChecksum, validateChecksum,
getInstalledClientVersion, getInstalledClientVersion,
fetchNewAPI, fetchMirrorManifest,
getLatestVersionFromNewAPI, getPWRUrl,
getPWRUrlFromNewAPI, getPWRUrlFromNewAPI,
extractVersionNumber 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

View File

@@ -0,0 +1,217 @@
# Patch CDN Infrastructure & Game Update System
## Overview
The F2P launcher downloads game patches through a CDN redirect gateway hosted on the auth server. This allows instant CDN switching (e.g., for DMCA takedowns) without releasing a new launcher version.
## Architecture
```
Launcher --> GET auth.sanasol.ws/patches/manifest.json
--> 302 --> mega.io/.../manifest.json
Launcher --> GET auth.sanasol.ws/patches/windows/amd64/release/0_to_11.pwr
--> 302 --> mega.io/.../windows/amd64/release/0_to_11.pwr
```
The auth server acts as a pure redirect gateway (302). No bandwidth is consumed on the auth server - all actual file transfers happen directly between the launcher and the CDN.
## URLs
| URL | Purpose |
|-----|---------|
| `https://auth.sanasol.ws/patches/*` | Redirect gateway (302 -> CDN) |
| `https://auth.sanasol.ws/patches/manifest.json` | Patch manifest (redirects to CDN) |
| `https://auth.sanasol.ws/admin/page/settings` | Admin panel to change CDN URL |
| `https://auth.sanasol.ws/admin/api/settings/patches-cdn` | API to GET/POST CDN base URL |
### Default CDN (MEGA S4)
```
Base URL: https://s3.g.s4.mega.io/kcvismkrtfcalgwxzsazbq46l72dwsypqaham/hytale/patches
```
### Changing CDN (DMCA response)
1. Go to `https://auth.sanasol.ws/admin/page/settings`
2. Find "Patches CDN Base URL" section
3. Change URL to new CDN (e.g., `https://new-cdn.example.com/patches`)
4. Click "Save" - all launcher requests instantly redirect to new CDN
5. No launcher update needed
## Manifest Format
The manifest is a JSON file listing all available patch files:
```json
{
"updated": "2026-02-20T13:20:09.776Z",
"files": {
"windows/amd64/release/0_to_11.pwr": { "size": 1618804736 },
"windows/amd64/release/10_to_11.pwr": { "size": 62914560 },
"darwin/arm64/release/0_to_11.pwr": { "size": 1617100800 },
"server/release": { "version": "2026.02.19-1a311a592", "size": 1509949440, "sha256": "..." },
...
}
}
```
### Key Structure
File keys follow the pattern: `{os}/{arch}/{branch}/{from}_to_{to}.pwr`
- **OS**: `windows`, `linux`, `darwin`
- **Arch**: `amd64`, `arm64`
- **Branch**: `release`, `pre-release`
- **Patch**: `{from}_to_{to}.pwr` (e.g., `0_to_11.pwr` for full install, `10_to_11.pwr` for differential)
Server builds use: `server/{branch}` with `version`, `size`, `sha256` fields.
## Game Update Process
### 1. Version Check
```
Launcher calls: getLatestClientVersion(branch)
-> Fetches manifest from auth.sanasol.ws/patches/manifest.json
-> Finds highest build number for current platform/branch
-> Returns "v{buildNumber}" (e.g., "v11")
```
### 2. Update Plan (Optimal Patch Routing)
```
Launcher calls: getUpdatePlan(currentBuild, targetBuild, branch)
-> Fetches manifest
-> Finds available patches for platform
-> Uses BFS to find optimal path (minimizes total download size)
-> Example: build 5 -> 11 might use: 5->10 (148MB) + 10->11 (60MB)
instead of: 0->11 (1.5GB)
```
### 3. Download & Apply
```
For each step in the update plan:
1. Download .pwr file from auth.sanasol.ws/patches/{key}
(redirects to CDN, supports resume via Range headers)
2. Apply patch using butler tool:
butler apply --staging-dir <staging> <pwr_file> <game_dir>
3. Save version after each step
```
### 4. Fresh Install
For first-time installs (currentBuild = 0):
- Downloads `0_to_{target}.pwr` (full install, ~1.5GB)
- Applies with butler to create the full game directory
### 5. Differential Update
For existing installations:
- Finds optimal patch chain (e.g., `10_to_11.pwr` at ~60MB)
- Applies incrementally, saving progress after each step
- Falls back to full install if no patch path found
## Mirror Sync Script
The mirror script (`scripts/hytale-mirror.js`) downloads patches from the official Hytale API and uploads to MEGA S4.
### Usage
```bash
cd scripts
node hytale-mirror.js download # Download patches locally
node hytale-mirror.js upload # Upload to MEGA S4 via rclone
node hytale-mirror.js sync # Download + Upload in one step
```
### What It Does
1. **Discovery**: Calls Hytale API to find available patches for all platforms
2. **Download**: Downloads .pwr files to `scripts/mirror/` directory
3. **Manifest Generation**: Creates `manifest.json` with file sizes (no local paths)
4. **Upload**: Uses `rclone` to sync to MEGA S4
### SOCKS5 Proxy
- API discovery calls use SOCKS5 proxy rotation (for rate limiting)
- File downloads do NOT use proxy (too slow for large files)
- Proxy list in `proxies.json` (auto-refreshed from proxy service)
### Prerequisites
- `rclone` configured with `megas4` remote pointing to MEGA S4
- Node.js 20+
- Network access to Hytale API endpoints
## Launcher Configuration
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `HYTALE_AUTH_DOMAIN` | `auth.sanasol.ws` | Auth domain (used for patch redirects) |
### Key Files
| File | Description |
|------|-------------|
| `backend/services/versionManager.js` | Manifest fetching, version checking, update planning |
| `backend/managers/differentialUpdateManager.js` | Download orchestration, butler integration |
| `backend/utils/fileManager.js` | File download with retry, resume, stall detection |
| `backend/managers/gameLauncher.js` | Game launch with token fetch, patching, signing |
### Constants (versionManager.js)
```javascript
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`;
const MANIFEST_CACHE_DURATION = 60000; // 1 minute cache
const FALLBACK_LATEST_BUILD = 11; // If manifest unreachable
```
## Auth Server Implementation
### Routes
```
GET /patches/* -> handlePatchRedirect()
- Extracts path after /patches/
- Reads CDN base URL from Redis settings
- Returns 302 redirect to {baseUrl}/{path}
- Tracks download metrics
GET /admin/api/settings/patches-cdn -> getPatchesCdnBaseUrl()
POST /admin/api/settings/patches-cdn -> setPatchesCdnBaseUrl()
```
### Redis Storage
```
settings:global -> { patchesCdnBaseUrl: "https://s3.g.s4.mega.io/..." }
metrics:downloads -> { "patch:manifest.json": count, ... }
```
## Troubleshooting
### "Invalid manifest structure" error
- Check manifest.json is valid JSON with `files` object
- Verify CDN is accessible: `curl -sL https://auth.sanasol.ws/patches/manifest.json | python3 -m json.tool`
- Check admin settings for correct CDN URL
### 0-byte downloads
- Verify redirect works: `curl -sI https://auth.sanasol.ws/patches/darwin/arm64/release/0_to_11.pwr`
- Should show `302` with `Location` header
- Test actual download: `curl -sL -o /dev/null -w "%{size_download}" -r 0-1023 <url>`
### Manifest has local paths
- Regenerate manifest: `node scripts/hytale-mirror.js download` (re-scans files)
- Re-upload: `node scripts/hytale-mirror.js upload`
- Verify: entries should only have `{ size: <bytes> }`, no `path` field
### CDN switch not taking effect
- Check Redis: CDN URL stored in `settings:global`
- Verify via API: `curl https://auth.sanasol.ws/admin/api/settings/patches-cdn`
- Manifest is cached for 1 minute in launcher - wait or restart

View File

@@ -1,6 +1,6 @@
{ {
"name": "hytale-f2p-launcher", "name": "hytale-f2p-launcher",
"version": "2.2.2", "version": "2.3.1",
"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"
} }
} }
} }