mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-27 07:11:47 -03:00
Compare commits
1 Commits
v2.3.3
...
macos-nota
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3c777eb43 |
2
.github/CODE_OF_CONDUCT.md
vendored
2
.github/CODE_OF_CONDUCT.md
vendored
@@ -36,7 +36,7 @@ This Code of Conduct applies within all community spaces, and also applies when
|
|||||||
|
|
||||||
## Enforcement
|
## Enforcement
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Discord Server, message Founders/Devs](https://discord.gg/Fhbb9Yk5WW). All complaints will be reviewed and investigated promptly and fairly.
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Discord Server, message Founders/Devs](https://discord.gg/hf2pdc). All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||||
|
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/support_request.yml
vendored
2
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -22,7 +22,7 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
If you need help or support with using the launcher, please fill out this support request.
|
If you need help or support with using the launcher, please fill out this support request.
|
||||||
Provide as much detail as possible so we can assist you effectively.
|
Provide as much detail as possible so we can assist you effectively.
|
||||||
**Need a quick assistance?** Please Open-A-Ticket in our [Discord Server](https://discord.gg/Fhbb9Yk5WW)!
|
**Need a quick assistance?** Please Open-A-Ticket in our [Discord Server](https://discord.gg/gME8rUy3MB)!
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: question
|
id: question
|
||||||
|
|||||||
196
.github/workflows/release.yml
vendored
196
.github/workflows/release.yml
vendored
@@ -6,117 +6,173 @@ 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:
|
||||||
create-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- 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:
|
build-windows:
|
||||||
needs: [create-release]
|
runs-on: windows-latest
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install Wine for cross-compilation
|
|
||||||
run: |
|
|
||||||
sudo dpkg --add-architecture i386
|
|
||||||
sudo mkdir -pm755 /etc/apt/keyrings
|
|
||||||
sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key
|
|
||||||
sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/$(lsb_release -cs)/winehq-$(lsb_release -cs).sources
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y --install-recommends winehq-stable
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
|
|
||||||
- name: Build Windows Packages
|
- name: Build Windows Packages
|
||||||
run: npx electron-builder --win --publish never --config.npmRebuild=false
|
run: npx electron-builder --win --publish never
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
- name: Upload to Release
|
with:
|
||||||
run: |
|
name: windows-builds
|
||||||
RELEASE_ID=$(curl -s "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
|
path: |
|
||||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
dist/*.exe
|
||||||
for file in dist/*.exe dist/*.exe.blockmap dist/latest.yml; do
|
dist/*.exe.blockmap
|
||||||
[ -f "$file" ] || continue
|
dist/latest.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-macos:
|
build-macos:
|
||||||
needs: [create-release]
|
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
|
|
||||||
- name: Build macOS Packages
|
- name: Build macOS Packages
|
||||||
env:
|
env:
|
||||||
|
# Code signing
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
|
# Notarization
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
run: npx electron-builder --mac --publish never
|
run: npx electron-builder --mac --publish never
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
- name: Upload to Release
|
with:
|
||||||
run: |
|
name: macos-builds
|
||||||
RELEASE_ID=$(curl -s "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
|
path: |
|
||||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
dist/*.dmg
|
||||||
for file in dist/*.dmg dist/*.zip dist/*.blockmap dist/latest-mac.yml; do
|
dist/*.zip
|
||||||
[ -f "$file" ] || continue
|
dist/*.blockmap
|
||||||
echo "Uploading $file..."
|
dist/latest-mac.yml
|
||||||
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 rpm
|
sudo apt-get install -y libarchive-tools
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
|
cache: 'npm'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
|
|
||||||
- name: Build Linux Packages
|
- name: Build Linux Packages
|
||||||
run: npx electron-builder --linux AppImage deb rpm pacman --publish never
|
|
||||||
|
|
||||||
- name: Upload to Release
|
|
||||||
run: |
|
run: |
|
||||||
RELEASE_ID=$(curl -s "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
|
npx electron-builder --linux AppImage deb rpm --publish never
|
||||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
- uses: actions/upload-artifact@v4
|
||||||
for file in dist/*.AppImage dist/*.AppImage.blockmap dist/*.deb dist/*.rpm dist/*.pacman dist/latest-linux.yml; do
|
with:
|
||||||
[ -f "$file" ] || continue
|
name: linux-builds
|
||||||
echo "Uploading $file..."
|
path: |
|
||||||
curl -s --max-time 600 -X POST "${FORGEJO_UPLOAD}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=$(basename $file)" \
|
dist/*.AppImage
|
||||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
dist/*.AppImage.blockmap
|
||||||
-F "attachment=@${file}" || echo "Failed to upload $file"
|
dist/*.deb
|
||||||
done
|
dist/*.rpm
|
||||||
|
dist/latest-linux.yml
|
||||||
|
|
||||||
|
build-arch:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: archlinux:latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install base packages
|
||||||
|
run: |
|
||||||
|
pacman -Syu --noconfirm
|
||||||
|
pacman -S --noconfirm \
|
||||||
|
base-devel \
|
||||||
|
git \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
rpm-tools \
|
||||||
|
libxcrypt-compat
|
||||||
|
|
||||||
|
- name: Create build user
|
||||||
|
run: |
|
||||||
|
useradd -m builder
|
||||||
|
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
|
||||||
|
|
||||||
|
- name: Fix Permissions
|
||||||
|
run: chown -R builder:builder .
|
||||||
|
|
||||||
|
- name: Build Arch Package
|
||||||
|
run: |
|
||||||
|
sudo -u builder bash << 'EOF'
|
||||||
|
set -e
|
||||||
|
makepkg --printsrcinfo > .SRCINFO
|
||||||
|
makepkg -s --noconfirm
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Fix permissions for upload
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
sudo chown -R $(id -u):$(id -g) .
|
||||||
|
|
||||||
|
- name: Upload Arch Package
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: arch-package
|
||||||
|
path: |
|
||||||
|
*.pkg.tar.zst
|
||||||
|
.SRCINFO
|
||||||
|
include-hidden-files: true
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: [build-windows, build-macos, build-linux, build-arch]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
startsWith(github.ref, 'refs/tags/v') ||
|
||||||
|
github.ref == 'refs/heads/main' ||
|
||||||
|
github.event_name == 'workflow_dispatch'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Display structure of downloaded files
|
||||||
|
run: ls -R artifacts
|
||||||
|
|
||||||
|
- name: Get version from package.json
|
||||||
|
id: pkg_version
|
||||||
|
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
files: |
|
||||||
|
artifacts/arch-package/*.pkg.tar.zst
|
||||||
|
artifacts/arch-package/.SRCINFO
|
||||||
|
artifacts/linux-builds/**/*
|
||||||
|
artifacts/windows-builds/**/*
|
||||||
|
artifacts/macos-builds/**/*
|
||||||
|
generate_release_notes: true
|
||||||
|
draft: true
|
||||||
|
prerelease: false
|
||||||
|
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,9 +17,6 @@ dist/
|
|||||||
# Project Specific: Downloaded patcher (from hytale-auth-server)
|
# Project Specific: Downloaded patcher (from hytale-auth-server)
|
||||||
backend/patcher/
|
backend/patcher/
|
||||||
|
|
||||||
# Private docs (local only)
|
|
||||||
docs/PATCH_CDN_INFRASTRUCTURE.md
|
|
||||||
|
|
||||||
# macOS Specific
|
# macOS Specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.zst.DS_Store
|
*.zst.DS_Store
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ window.closeDiscordPopup = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.joinDiscord = async function() {
|
window.joinDiscord = async function() {
|
||||||
await window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
|
await window.electronAPI?.openExternal('https://discord.gg/hf2pdc');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.electronAPI?.saveConfig({ discordPopup: true });
|
await window.electronAPI?.saveConfig({ discordPopup: true });
|
||||||
|
|||||||
@@ -1103,7 +1103,7 @@ function getRetryContextMessage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.openDiscordExternal = function() {
|
window.openDiscordExternal = function() {
|
||||||
window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
|
window.electronAPI?.openExternal('https://discord.gg/hf2pdc');
|
||||||
};
|
};
|
||||||
|
|
||||||
window.toggleMaximize = toggleMaximize;
|
window.toggleMaximize = toggleMaximize;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
### ⚠️ **WARNING: READ [QUICK START](#-quick-start) before Downloading & Installing the Launcher!** ⚠️
|
### ⚠️ **WARNING: READ [QUICK START](#-quick-start) before Downloading & Installing the Launcher!** ⚠️
|
||||||
|
|
||||||
#### 🛑 **Found a problem? [Join the HF2P Discord](https://discord.gg/Fhbb9Yk5WW) and head to `#-⚠️-community-help`** 🛑
|
#### 🛑 **Found a problem? [Join the HF2P Discord](https://discord.gg/hf2pdc) and head to `#-⚠️-community-help`** 🛑
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
👍 If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> ☕<br>
|
👍 If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> ☕<br>
|
||||||
@@ -455,7 +455,7 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
**Questions? Ads? Collaboration? Endorsement? Other business-related?**
|
**Questions? Ads? Collaboration? Endorsement? Other business-related?**
|
||||||
Message the founders at https://discord.gg/Fhbb9Yk5WW
|
Message the founders at https://discord.gg/hf2pdc
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
128
SERVER.md
128
SERVER.md
@@ -2,20 +2,19 @@
|
|||||||
|
|
||||||
Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup.
|
Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup.
|
||||||
|
|
||||||
### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/Fhbb9Yk5WW**
|
### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/hf2pdc**
|
||||||
|
|
||||||
**Table of Contents**
|
**Table of Contents**
|
||||||
|
|
||||||
* [\[NEW!\] Play Online with Official Accounts 🆕](#new-play-online-with-official-accounts-)
|
|
||||||
* ["Server" Term and Definition](#server-term-and-definiton)
|
* ["Server" Term and Definition](#server-term-and-definiton)
|
||||||
* [Server Directory Location](#server-directory-location)
|
* [Server Directory Location](#server-directory-location)
|
||||||
* [A. Host Your Singleplayer World](#a-host-your-singleplayer-world)
|
* [A. Online Play Feature](#a-online-play-feature)
|
||||||
* [1. Using Online-Play Feature In-Game Invite Code](#1-using-online-play-feature--in-game-invite-code)
|
* [1. Host Your Singleplayer World using In-Game Invite Code](#1-host-your-singleplayer-world-using-in-game-invite-code)
|
||||||
* [Common Issues (UPnP/NAT/STUN) on Online Play](#common-issues-upnpnatstun-on-online-play)
|
* [Common Issues (UPnP/NAT/STUN) on Online Play](#common-issues-upnpnatstun-on-online-play)
|
||||||
* [2. Using Tailscale](#2-using-tailscale)
|
* [2. Host Your Singleplayer World using Tailscale](#2-host-your-singleplayer-world-using-tailscale)
|
||||||
* [3. Using Radmin VPN](#3-using-radmin-vpn)
|
|
||||||
* [B. Local Dedicated Server](#b-local-dedicated-server)
|
* [B. Local Dedicated Server](#b-local-dedicated-server)
|
||||||
* [1. Using Playit.gg (Recommended) ✅](#1-using-playitgg-recommended-)
|
* [1. Using Playit.gg (Recommended) ✅](#1-using-playitgg-recommended-)
|
||||||
|
* [2. Using Radmin VPN](#2-using-radmin-vpn)
|
||||||
* [C. 24/7 Dedicated Server (Advanced)](#c-247-dedicated-server-advanced)
|
* [C. 24/7 Dedicated Server (Advanced)](#c-247-dedicated-server-advanced)
|
||||||
* [Step 1: Get the Files Ready](#step-1-get-the-files-ready)
|
* [Step 1: Get the Files Ready](#step-1-get-the-files-ready)
|
||||||
* [Step 2: Place HytaleServer.jar in the Server directory](#step-2-place-hytaleserverjar-in-the-server-directory)
|
* [Step 2: Place HytaleServer.jar in the Server directory](#step-2-place-hytaleserverjar-in-the-server-directory)
|
||||||
@@ -33,69 +32,6 @@ Play with friends online! This guide covers both easy in-game hosting and advanc
|
|||||||
* [10. Getting Help](#10-getting-help)
|
* [10. Getting Help](#10-getting-help)
|
||||||
---
|
---
|
||||||
|
|
||||||
<div align='center'>
|
|
||||||
<h3>
|
|
||||||
<b>
|
|
||||||
Do you want to create Hytale Game Server with EASY SETUP, AFFORDABLE PRICE, AND 24/7 SUPPORT?
|
|
||||||
</b>
|
|
||||||
</h3>
|
|
||||||
<h2>
|
|
||||||
<b>
|
|
||||||
<a href="https://cloudnord.net/hytale-server-hosting">CLOUDNORD</a> is the ANSWER! HF2P Server is available!
|
|
||||||
</b>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
**CloudNord's Hytale, Minecraft, and Game Hosting** is at the core of our Server Hosting business. Join our Gaming community and experience our large choice of premium game servers, we’ve got you covered with super high-performance hardware, fantastic support options, and powerful server hosting to build and explore your worlds without limits!
|
|
||||||
|
|
||||||
**Order your Hytale, Minecraft, or other game servers today!**
|
|
||||||
Choose Java Edition, Bedrock Edition, Cross-Play, or any of our additional supported games.
|
|
||||||
Enjoy **20% OFF** all new game servers, **available now for a limited time!** Don’t miss out.
|
|
||||||
|
|
||||||
### **CloudNord key hosting features include:**
|
|
||||||
- Instant Server Setup ⚡
|
|
||||||
- High Performance Game Servers 🚀
|
|
||||||
- Game DDoS Protection 🛡️
|
|
||||||
- Intelligent Game Backups 🧠
|
|
||||||
- Quick Modpack Installer 🔧
|
|
||||||
- Quick Plugin & Mod Installer 🧰
|
|
||||||
- Full File Access 🗃️
|
|
||||||
- 24/7 Support 📞 🏪
|
|
||||||
- Powerful Game Control Server Panel 💪
|
|
||||||
|
|
||||||
### **Check Us Out:**
|
|
||||||
* 👉 CloudNord Website: https://cloudnord.net/hytalef2p
|
|
||||||
* 👉 CloudNord Discord: https://discord.gg/TYxGrmUz4Y
|
|
||||||
* 👉 CloudNord Reviews: https://www.trustpilot.com/review/cloudnord.net?page=2&stars=5
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### [NEW!] Play Online with Official Accounts 🆕
|
|
||||||
|
|
||||||
**Documentations:**
|
|
||||||
* [Hytale-Server-Docker by Sanasol](https://github.com/sanasol/hytale-server-docker/tree/main?tab=readme-ov-file#dual-authentication)
|
|
||||||
|
|
||||||
**Requirements:**
|
|
||||||
* Using the patched HytaleServer.jar
|
|
||||||
* Has Official Account with Purchased status on Official Hytale Website.
|
|
||||||
* This official account holder can be the server hoster or one of the players.
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Running the patched HytaleServer.jar with either [B. Local Dedicated Server](#b-local-dedicated-server) or [C. 24/7 Dedicated Server (Advanced)](#c-247-dedicated-server-advanced) successfully.
|
|
||||||
2. On the server's console/terminal/CMD, server admin **MUST RUN THIS EACH BOOT** to allow players with Official Hytale game license to connect on the server:
|
|
||||||
```
|
|
||||||
/auth logout
|
|
||||||
/auth persistence Encrypted
|
|
||||||
/auth login device
|
|
||||||
```
|
|
||||||
3. Server console will show instructions, an URL and a code; these will be revoked after 10 minutes if not authorized.
|
|
||||||
4. The server hoster can open the URL directly to browser by holding Ctrl then Click on it, or copy and send it to the player with official account.
|
|
||||||
5. Once it authorized, the official accounts can join server with F2P players.
|
|
||||||
6. If you want to modify anything, look at the [Hytale-Server-Docker](https://github.com/sanasol/hytale-server-docker/) above, give the repo a STAR too.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### "Server" Term and Definiton
|
### "Server" Term and Definiton
|
||||||
|
|
||||||
"HytaleServer.jar", which called as "Server", functions as the place of authentication of the client that supposed to go to Hytale Official Authentication System but we managed our way to redirect it on our service (Thanks to Sanasol), handling approximately thousands of players worldwide to play this game for free.
|
"HytaleServer.jar", which called as "Server", functions as the place of authentication of the client that supposed to go to Hytale Official Authentication System but we managed our way to redirect it on our service (Thanks to Sanasol), handling approximately thousands of players worldwide to play this game for free.
|
||||||
@@ -105,15 +41,14 @@ Kindly support us via [our Buy Me a Coffee link](https://buymeacoffee.com/hf2p)
|
|||||||
|
|
||||||
### Server Directory Location
|
### Server Directory Location
|
||||||
|
|
||||||
Here are the directory locations of Server folder if you have installed it on default instalation location:
|
Here are the directory locations of Server folder if you have installed
|
||||||
- **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server`
|
- **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server`
|
||||||
- **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
|
- **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server`
|
||||||
- **Linux:** `~/.hytalef2p/release/package/game/latest/Server`
|
- **Linux:** `~/.hytalef2p/release/package/game/latest/Server`
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> This location only exists if the user installed the game using our launcher.
|
> This location only exists if the user installed the game using our launcher. The `Server` folder needed to auth the HytaleClient to play Hytale online
|
||||||
> The `Server` folder needed to auth the HytaleClient to play Hytale in Singleplayer/Multiplayer for now.
|
> (for now; we planned to add offline mode in later version of our launcher).
|
||||||
> (We planned to add offline mode in later version of our launcher).
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Hosting a dedicated Hytale server will not need the exact similar tree. You can put it anywhere, as long as the directory has `Assets.zip` which
|
> Hosting a dedicated Hytale server will not need the exact similar tree. You can put it anywhere, as long as the directory has `Assets.zip` which
|
||||||
@@ -129,7 +64,6 @@ Terms and conditions applies.
|
|||||||
## 1. Using Online-Play Feature / In-Game Invite Code
|
## 1. Using Online-Play Feature / In-Game Invite Code
|
||||||
|
|
||||||
The easiest way to play with friends - no manual server setup required!
|
The easiest way to play with friends - no manual server setup required!
|
||||||
|
|
||||||
*The game automatically handles networking using UPnP/STUN/NAT traversal.*
|
*The game automatically handles networking using UPnP/STUN/NAT traversal.*
|
||||||
|
|
||||||
**For Online Play to work, you need:**
|
**For Online Play to work, you need:**
|
||||||
@@ -178,7 +112,6 @@ Warning: Your network configuration may prevent other players from connecting.
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details><summary><b>b. "UPnP Failed" or "Port Mapping Failed" Warning</b></summary>
|
<details><summary><b>b. "UPnP Failed" or "Port Mapping Failed" Warning</b></summary>
|
||||||
|
|
||||||
**Check your router:**
|
**Check your router:**
|
||||||
1. Log into router admin panel (usually `192.168.1.1` or `192.168.0.1`)
|
1. Log into router admin panel (usually `192.168.1.1` or `192.168.0.1`)
|
||||||
2. Find UPnP settings (often under "Advanced" or "NAT")
|
2. Find UPnP settings (often under "Advanced" or "NAT")
|
||||||
@@ -190,8 +123,7 @@ Warning: Your network configuration may prevent other players from connecting.
|
|||||||
- See "Port Forwarding" or "Workarounds or NAT/CGNAT" sections below
|
- See "Port Forwarding" or "Workarounds or NAT/CGNAT" sections below
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details><summary><b>c. "Connected via STUN", "Strict NAT" or "Symmetric NAT" Warning</b></summary>
|
<details><summary><b>c. "Strict NAT" or "Symmetric NAT" Warning</b></summary>
|
||||||
|
|
||||||
Some routers have restrictive NAT that blocks peer connections.
|
Some routers have restrictive NAT that blocks peer connections.
|
||||||
|
|
||||||
**Try:**
|
**Try:**
|
||||||
@@ -201,7 +133,6 @@ Some routers have restrictive NAT that blocks peer connections.
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
## 2. Using Tailscale
|
## 2. Using Tailscale
|
||||||
|
|
||||||
Tailscale creates mesh VPN service that streamlines connecting devices and services securely across different networks. And **works crossplatform!!**
|
Tailscale creates mesh VPN service that streamlines connecting devices and services securely across different networks. And **works crossplatform!!**
|
||||||
|
|
||||||
1. All members are required to download [Tailscale](https://tailscale.com/download) on your device.
|
1. All members are required to download [Tailscale](https://tailscale.com/download) on your device.
|
||||||
@@ -217,17 +148,6 @@ Tailscale creates mesh VPN service that streamlines connecting devices and servi
|
|||||||
* Use the new share code to connect
|
* Use the new share code to connect
|
||||||
* To test your connection, ping the host's ipv4 mentioned in Tailscale
|
* To test your connection, ping the host's ipv4 mentioned in Tailscale
|
||||||
|
|
||||||
## 3. Using Radmin VPN
|
|
||||||
|
|
||||||
Creates a virtual LAN - all players need to install it:
|
|
||||||
|
|
||||||
1. Download [Radmin VPN](https://www.radmin-vpn.com/) - All players install it
|
|
||||||
2. One person create a room/network, others join with network name/password
|
|
||||||
3. Host joined the world, others will connect to it.
|
|
||||||
4. Open Hytale Game > Servers > Add Servers > Direct Connect > Type IP Address of the Host from Radmin.
|
|
||||||
|
|
||||||
These options bypass all NAT/CGNAT issues. But for **Windows machines only!**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# B. Local Dedicated Server
|
# B. Local Dedicated Server
|
||||||
@@ -247,12 +167,11 @@ Free tunneling service - only the host needs to install it:
|
|||||||
* Right-click file > Properties > Turn on 'Executable as a Program' | or `chmod +x playit-linux-amd64` on terminal
|
* Right-click file > Properties > Turn on 'Executable as a Program' | or `chmod +x playit-linux-amd64` on terminal
|
||||||
* Run by double-clicking the file or `./playit-linux-amd64` via terminal
|
* Run by double-clicking the file or `./playit-linux-amd64` via terminal
|
||||||
5. Open the URL/link by `Ctrl+Click` it. If unable, select the URL, then Right-Click to Copy (`Ctrl+Shift+C` for Linux) then Paste the URL into your browser to link it with your created account.
|
5. Open the URL/link by `Ctrl+Click` it. If unable, select the URL, then Right-Click to Copy (`Ctrl+Shift+C` for Linux) then Paste the URL into your browser to link it with your created account.
|
||||||
6. Once it done, download the `run_server_with_tokens (1)` script file (`.BAT` for Windows, `.SH` for Linux) from our Discord server > channel `#open-public-server`
|
6. **WARNING: Do not close the terminal if you are still playing or hosting the server**
|
||||||
7. Put the script file to the `Server` folder in `HytaleF2P` directory (`%localappdata%\HytaleF2P\release\package\game\latest\Server`)
|
7. Once it done, download the `run_server_with_tokens` script file (`.BAT` for Windows, `.SH` for Linux) from our Discord server > channel `#open-public-server`
|
||||||
8. Rename the script file to `run_server_with_tokens` to make it easier if you run it with Terminal, then do Method A or B.
|
8. Put the script file to the `Server` folder in `HytaleF2P` directory (`%localappdata%\HytaleF2P\release\package\game\latest\Server`)
|
||||||
9. If you put it in `Server` folder in `HytaleF2P` launcher, change `ASSETS_PATH="${ASSETS_PATH:-./Assets.zip}"` inside the script to be `ASSETS_PATH="${ASSETS_PATH:-../Assets.zip}"`. NOTICE THE `./` and `../` DIFFERENCE.
|
9. Copy the `Assets.zip` from the `%localappdata%\HytaleF2P\release\package\game\latest\` folder to the `Server\` folder. (TIP: You can use Symlink of that file to reduce disk usage!)
|
||||||
10. Copy the `Assets.zip` from the `%localappdata%\HytaleF2P\release\package\game\latest\` folder to the `Server\` folder. (TIP: You can use Symlink of that file to reduce disk usage!)
|
10. Double-click the .BAT file to host your server, wait until it shows:
|
||||||
11. Double-click the .BAT file to host your server, wait until it shows:
|
|
||||||
```
|
```
|
||||||
===================================================
|
===================================================
|
||||||
Hytale Server Booted! [Multiplayer, Fresh Universe]
|
Hytale Server Booted! [Multiplayer, Fresh Universe]
|
||||||
@@ -261,12 +180,16 @@ Hytale Server Booted! [Multiplayer, Fresh Universe]
|
|||||||
11. Connect to the server by go to `Servers` in your game client, press `Add Server`, type `localhost` in the address box, use any name for your server.
|
11. Connect to the server by go to `Servers` in your game client, press `Add Server`, type `localhost` in the address box, use any name for your server.
|
||||||
12. Send the public address in Step 3 to your friends.
|
12. Send the public address in Step 3 to your friends.
|
||||||
|
|
||||||
> [!CAUTION]
|
## 2. Using Radmin VPN
|
||||||
> Do not close the Playit.gg Terminal OR HytaleServer Terminal if you are still playing or hosting the server.
|
|
||||||
|
|
||||||
## 2. Using Tailscale [DRAFT]
|
Creates a virtual LAN - all players need to install it:
|
||||||
|
|
||||||
Tailscale
|
1. Download [Radmin VPN](https://www.radmin-vpn.com/) - All players install it
|
||||||
|
2. One person create a room/network, others join with network name/password
|
||||||
|
3. Host joined the world, others will connect to it.
|
||||||
|
4. Open Hytale Game > Servers > Add Servers > Direct Connect > Type IP Address of the Host from Radmin.
|
||||||
|
|
||||||
|
These options bypass all NAT/CGNAT issues. But for **Windows machines only!**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -305,12 +228,12 @@ For 24/7 servers, custom configurations, or hosting on a VPS/dedicated machine.
|
|||||||
|
|
||||||
**Windows:**
|
**Windows:**
|
||||||
```batch
|
```batch
|
||||||
run_server_with_token.bat
|
run_server.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
**macOS / Linux:**
|
**macOS / Linux:**
|
||||||
```bash
|
```bash
|
||||||
./run_server_with_token.sh
|
./run_server.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -580,6 +503,3 @@ See [Docker documentation](https://github.com/Hybrowse/hytale-server-docker) for
|
|||||||
- Auth Server: sanasol.ws
|
- Auth Server: sanasol.ws
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Hytale F2P Launcher - Troubleshooting Guide
|
# Hytale F2P Launcher - Troubleshooting Guide
|
||||||
|
|
||||||
This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [Discord](https://discord.gg/Fhbb9Yk5WW).
|
This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [Discord](https://discord.gg/gME8rUy3MB).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -437,7 +437,7 @@ Game sessions have a 10-hour TTL. This is by design for security.
|
|||||||
If your issue isn't resolved by this guide:
|
If your issue isn't resolved by this guide:
|
||||||
|
|
||||||
1. **Check existing issues:** [GitHub Issues](https://github.com/amiayweb/Hytale-F2P/issues)
|
1. **Check existing issues:** [GitHub Issues](https://github.com/amiayweb/Hytale-F2P/issues)
|
||||||
2. **Join Discord:** [discord.gg/Fhbb9Yk5WW](https://discord.gg/Fhbb9Yk5WW)
|
2. **Join Discord:** [discord.gg/gME8rUy3MB](https://discord.gg/gME8rUy3MB)
|
||||||
3. **Open a new issue** with:
|
3. **Open a new issue** with:
|
||||||
- Your operating system and version
|
- Your operating system and version
|
||||||
- Launcher version
|
- Launcher version
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ 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) {
|
||||||
@@ -18,34 +14,6 @@ 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
|
||||||
@@ -248,10 +216,8 @@ class AppUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkForUpdatesAndNotify() {
|
checkForUpdatesAndNotify() {
|
||||||
// Resolve latest release URL then check for updates
|
// Check for updates and notify if available
|
||||||
this._resolveUpdateUrl().catch(err => {
|
autoUpdater.checkForUpdatesAndNotify().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
|
||||||
@@ -279,10 +245,8 @@ class AppUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkForUpdates() {
|
checkForUpdates() {
|
||||||
// Manual check - resolve latest release URL first
|
// Manual check for updates (returns promise)
|
||||||
return this._resolveUpdateUrl().catch(err => {
|
return autoUpdater.checkForUpdates().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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const FORCE_CLEAN_INSTALL_VERSION = false;
|
const FORCE_CLEAN_INSTALL_VERSION = false;
|
||||||
const CLEAN_INSTALL_TEST_VERSION = 'v4';
|
const CLEAN_INSTALL_TEST_VERSION = '4.pwr';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
FORCE_CLEAN_INSTALL_VERSION,
|
FORCE_CLEAN_INSTALL_VERSION,
|
||||||
|
|||||||
@@ -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, getInstalledClientVersion, getUpdatePlan, extractVersionNumber } = require('../services/versionManager');
|
const { validateChecksum, extractVersionDetails, canUseDifferentialUpdate, needsIntermediatePatches, getInstalledClientVersion } = require('../services/versionManager');
|
||||||
const { installButler } = require('./butlerManager');
|
const { installButler } = require('./butlerManager');
|
||||||
const { GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
const { GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
||||||
const { saveVersionClient } = require('../core/config');
|
const { saveVersionClient } = require('../core/config');
|
||||||
@@ -156,15 +156,15 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
|
|||||||
console.log(`Initiating intelligent update to version ${targetVersion}`);
|
console.log(`Initiating intelligent update to version ${targetVersion}`);
|
||||||
|
|
||||||
const currentVersion = getInstalledClientVersion();
|
const currentVersion = getInstalledClientVersion();
|
||||||
const currentBuild = extractVersionNumber(currentVersion) || 0;
|
console.log(`Current version: ${currentVersion || 'none (clean install)'}`);
|
||||||
const targetBuild = extractVersionNumber(targetVersion);
|
console.log(`Target version: ${targetVersion}`);
|
||||||
console.log(`Current build: ${currentBuild}, Target build: ${targetBuild}, Branch: ${branch}`);
|
console.log(`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 archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
|
const archiveName = path.basename(versionDetails.fullUrl);
|
||||||
|
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Downloading full game archive (pre-release)...', 0, null, null, null);
|
progressCallback('Downloading full game archive (pre-release)...', 0, null, null, null);
|
||||||
@@ -177,14 +177,14 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean install (no current version)
|
if (!currentVersion) {
|
||||||
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 archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
|
const archiveName = path.basename(versionDetails.fullUrl);
|
||||||
|
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Downloading full game archive (first install - v${targetBuild})...`, 0, null, null, null);
|
progressCallback(`Downloading full game archive (first install - v${targetVersion})...`, 0, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||||
@@ -194,67 +194,59 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Already at target
|
const patchesToApply = needsIntermediatePatches(currentVersion, targetVersion);
|
||||||
if (currentBuild >= targetBuild) {
|
|
||||||
console.log('Already at target version or newer');
|
if (patchesToApply.length === 0) {
|
||||||
|
console.log('Already at target version or invalid version sequence');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use mirror's update plan for optimal patch routing
|
console.log(`Applying ${patchesToApply.length} differential patch(es): ${patchesToApply.join(' -> ')}`);
|
||||||
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 < patchesToApply.length; i++) {
|
||||||
|
const patchVersion = patchesToApply[i];
|
||||||
|
const versionDetails = await extractVersionDetails(patchVersion, branch);
|
||||||
|
|
||||||
for (let i = 0; i < plan.steps.length; i++) {
|
const canDifferential = canUseDifferentialUpdate(getInstalledClientVersion(), versionDetails);
|
||||||
const step = plan.steps[i];
|
|
||||||
const stepName = `${step.from}_to_${step.to}`;
|
if (!canDifferential || !versionDetails.differentialUrl) {
|
||||||
const archivePath = path.join(cacheDir, `${branch}_${stepName}.pwr`);
|
console.log(`WARNING: Differential patch not available for ${patchVersion}, using full archive`);
|
||||||
const isDifferential = step.from !== 0;
|
const archiveName = path.basename(versionDetails.fullUrl);
|
||||||
|
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Downloading patch ${i + 1}/${plan.steps.length}: ${stepName}...`, 0, null, null, null);
|
progressCallback(`Downloading full archive for ${patchVersion} (${i + 1}/${patchesToApply.length})...`, 0, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
await acquireGameArchive(step.url, archivePath, null, progressCallback);
|
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||||
|
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||||
|
} else {
|
||||||
|
console.log(`Applying differential patch: ${versionDetails.sourceVersion} -> ${patchVersion}`);
|
||||||
|
const archiveName = path.basename(versionDetails.differentialUrl);
|
||||||
|
const archivePath = path.join(cacheDir, `${branch}_patch_${archiveName}`);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Applying patch ${i + 1}/${plan.steps.length}: ${stepName}...`, 50, null, null, null);
|
progressCallback(`Applying patch ${i + 1}/${patchesToApply.length}: ${patchVersion}...`, 0, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, isDifferential);
|
await acquireGameArchive(versionDetails.differentialUrl, archivePath, versionDetails.checksum, progressCallback);
|
||||||
|
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: ${stepName}.pwr`);
|
console.log(`Cleaned up patch file: ${archiveName}`);
|
||||||
} catch (cleanupErr) {
|
} catch (cleanupErr) {
|
||||||
console.warn(`Failed to cleanup: ${cleanupErr.message}`);
|
console.warn(`Failed to cleanup patch file: ${cleanupErr.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveVersionClient(`v${step.to}`);
|
|
||||||
console.log(`Patch ${stepName} applied (${i + 1}/${plan.steps.length})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Update completed. Version ${targetVersion} is now installed.`);
|
saveVersionClient(patchVersion);
|
||||||
} catch (planError) {
|
console.log(`Patch ${patchVersion} applied successfully (${i + 1}/${patchesToApply.length})`);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Update completed successfully. Version ${targetVersion} is now installed.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureGameInstalled(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) {
|
async function ensureGameInstalled(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) {
|
||||||
|
|||||||
@@ -61,39 +61,12 @@ async function fetchAuthTokens(uuid, name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const identityToken = data.IdentityToken || data.identityToken;
|
|
||||||
const sessionToken = data.SessionToken || data.sessionToken;
|
|
||||||
|
|
||||||
// Verify the identity token has the correct username
|
|
||||||
// This catches cases where the auth server defaults to "Player"
|
|
||||||
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');
|
console.log('Auth tokens received from server');
|
||||||
return { identityToken, sessionToken };
|
|
||||||
|
return {
|
||||||
|
identityToken: data.IdentityToken || data.identityToken,
|
||||||
|
sessionToken: data.SessionToken || data.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
|
||||||
@@ -439,7 +412,7 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
// This enables runtime auth patching without modifying the server JAR
|
// This enables runtime auth patching without modifying the server JAR
|
||||||
const agentJar = path.join(gameLatest, 'Server', 'dualauth-agent.jar');
|
const agentJar = path.join(gameLatest, 'Server', 'dualauth-agent.jar');
|
||||||
if (fs.existsSync(agentJar)) {
|
if (fs.existsSync(agentJar)) {
|
||||||
const agentFlag = `-javaagent:"${agentJar}"`;
|
const agentFlag = `-javaagent:${agentJar}`;
|
||||||
env.JAVA_TOOL_OPTIONS = env.JAVA_TOOL_OPTIONS
|
env.JAVA_TOOL_OPTIONS = env.JAVA_TOOL_OPTIONS
|
||||||
? `${env.JAVA_TOOL_OPTIONS} ${agentFlag}`
|
? `${env.JAVA_TOOL_OPTIONS} ${agentFlag}`
|
||||||
: agentFlag;
|
: agentFlag;
|
||||||
|
|||||||
@@ -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, getUpdatePlan, extractVersionNumber } = require('../services/versionManager');
|
const { getLatestClientVersion, getInstalledClientVersion } = 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, directUrl = null, expectedSize = null) {
|
async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
||||||
const osName = getOS();
|
const osName = getOS();
|
||||||
const arch = getArch();
|
const arch = getArch();
|
||||||
|
|
||||||
@@ -72,68 +72,28 @@ 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.');
|
||||||
}
|
}
|
||||||
|
|
||||||
let url;
|
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${branch}/0/${fileName}`;
|
||||||
|
const dest = path.join(cacheDir, `${branch}_${fileName}`);
|
||||||
if (directUrl) {
|
|
||||||
url = directUrl;
|
|
||||||
console.log(`[DownloadPWR] Using direct URL: ${url}`);
|
|
||||||
} else {
|
|
||||||
const { getPWRUrl } = require('../services/versionManager');
|
|
||||||
try {
|
|
||||||
console.log(`[DownloadPWR] Fetching mirror URL for branch: ${branch}, version: ${fileName}`);
|
|
||||||
url = await getPWRUrl(branch, fileName);
|
|
||||||
console.log(`[DownloadPWR] Mirror URL: ${url}`);
|
|
||||||
} 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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look up expected file size from manifest if not provided
|
|
||||||
if (!expectedSize) {
|
|
||||||
try {
|
|
||||||
const { fetchMirrorManifest } = require('../services/versionManager');
|
|
||||||
const manifest = await fetchMirrorManifest();
|
|
||||||
// Try to match: "0_to_11" format or "v11" format
|
|
||||||
const versionMatch = fileName.match(/^(\d+)_to_(\d+)$/);
|
|
||||||
let manifestKey;
|
|
||||||
if (versionMatch) {
|
|
||||||
manifestKey = `${osName}/${arch}/${branch}/${fileName}.pwr`;
|
|
||||||
} else {
|
|
||||||
const buildNum = extractVersionNumber(fileName);
|
|
||||||
manifestKey = `${osName}/${arch}/${branch}/0_to_${buildNum}.pwr`;
|
|
||||||
}
|
|
||||||
if (manifest.files[manifestKey]) {
|
|
||||||
expectedSize = manifest.files[manifestKey].size;
|
|
||||||
console.log(`[PWR] Expected size from manifest: ${(expectedSize / 1024 / 1024).toFixed(2)} MB`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(`[PWR] Could not fetch expected size from manifest: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dest = path.join(cacheDir, `${branch}_${fileName}.pwr`);
|
|
||||||
|
|
||||||
// Check if file exists and validate it
|
// 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) {
|
||||||
// Validate against expected size - reject if file is truncated (< 99% of expected)
|
return false;
|
||||||
if (expectedSize && stats.size < expectedSize * 0.99) {
|
}
|
||||||
console.log(`[PWR] Cached file truncated: ${(stats.size / 1024 / 1024).toFixed(2)} MB, expected ${(expectedSize / 1024 / 1024).toFixed(2)} MB. Deleting and re-downloading.`);
|
|
||||||
fs.unlinkSync(dest);
|
// Check if file is under 1.5 GB (incomplete download)
|
||||||
} else {
|
const sizeInMB = stats.size / 1024 / 1024;
|
||||||
console.log(`[PWR] Using cached file: ${dest} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
if (sizeInMB < 1500) {
|
||||||
return dest;
|
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
|
||||||
}
|
return false;
|
||||||
} else {
|
|
||||||
console.log(`[PWR] Cached file too small (${stats.size} bytes), re-downloading`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DownloadPWR] Downloading from: ${url}`);
|
console.log('Fetching PWR patch file:', url);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (manualRetry) {
|
if (manualRetry) {
|
||||||
@@ -159,7 +119,7 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
|
|||||||
const retryStats = fs.statSync(dest);
|
const retryStats = fs.statSync(dest);
|
||||||
console.log(`PWR file downloaded (auto-retry), size: ${(retryStats.size / 1024 / 1024).toFixed(2)} MB`);
|
console.log(`PWR file downloaded (auto-retry), size: ${(retryStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
if (!validatePWRFile(dest, expectedSize)) {
|
if (!validatePWRFile(dest)) {
|
||||||
console.log(`[PWR Validation] PWR file validation failed after auto-retry, deleting corrupted file: ${dest}`);
|
console.log(`[PWR Validation] PWR file validation failed after auto-retry, deleting corrupted file: ${dest}`);
|
||||||
fs.unlinkSync(dest);
|
fs.unlinkSync(dest);
|
||||||
throw new Error('Downloaded PWR file is corrupted or invalid after automatic retry. Please retry manually');
|
throw new Error('Downloaded PWR file is corrupted or invalid after automatic retry. Please retry manually');
|
||||||
@@ -210,7 +170,7 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
|
|||||||
const stats = fs.statSync(dest);
|
const stats = fs.statSync(dest);
|
||||||
console.log(`PWR file downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
console.log(`PWR file downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
if (!validatePWRFile(dest, expectedSize)) {
|
if (!validatePWRFile(dest)) {
|
||||||
console.log(`[PWR Validation] PWR file validation failed, deleting corrupted file: ${dest}`);
|
console.log(`[PWR Validation] PWR file validation failed, deleting corrupted file: ${dest}`);
|
||||||
fs.unlinkSync(dest);
|
fs.unlinkSync(dest);
|
||||||
throw new Error('Downloaded PWR file is corrupted or invalid. Please retry');
|
throw new Error('Downloaded PWR file is corrupted or invalid. Please retry');
|
||||||
@@ -228,7 +188,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, skipExistingCheck = false) {
|
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, branch = 'release', cacheDir = CACHE_DIR) {
|
||||||
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')}`);
|
||||||
@@ -252,12 +212,11 @@ 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');
|
||||||
|
|
||||||
if (!skipExistingCheck) {
|
const clientPath = findClientPath(gameLatest);
|
||||||
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
|
||||||
@@ -438,119 +397,58 @@ 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})`);
|
||||||
|
|
||||||
// Determine update strategy: intermediate patches vs full reinstall
|
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
|
||||||
const currentVersion = loadVersionClient();
|
|
||||||
const currentBuild = extractVersionNumber(currentVersion) || 0;
|
|
||||||
const targetBuild = extractVersionNumber(newVersion);
|
|
||||||
|
|
||||||
let useIntermediatePatches = false;
|
if (fs.existsSync(tempUpdateDir)) {
|
||||||
let updatePlan = null;
|
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(tempUpdateDir, { recursive: true });
|
||||||
|
|
||||||
if (currentBuild > 0 && currentBuild < targetBuild) {
|
if (progressCallback) {
|
||||||
try {
|
progressCallback('Downloading new game version...', 20, null, null, null);
|
||||||
updatePlan = await getUpdatePlan(currentBuild, targetBuild, branch);
|
|
||||||
useIntermediatePatches = !updatePlan.isFullInstall;
|
|
||||||
if (useIntermediatePatches) {
|
|
||||||
const totalMB = (updatePlan.totalSize / 1024 / 1024).toFixed(0);
|
|
||||||
console.log(`[UpdateGameFiles] Using intermediate patches: ${updatePlan.steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')} (${totalMB} MB)`);
|
|
||||||
}
|
|
||||||
} catch (planError) {
|
|
||||||
console.warn('[UpdateGameFiles] Could not get update plan, falling back to full install:', planError.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useIntermediatePatches && updatePlan) {
|
const pwrFile = await downloadPWR(branch, newVersion, progressCallback, cacheDir);
|
||||||
// Apply intermediate patches directly to game dir
|
|
||||||
for (let i = 0; i < updatePlan.steps.length; i++) {
|
|
||||||
const step = updatePlan.steps[i];
|
|
||||||
const stepName = `${step.from}_to_${step.to}`;
|
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
const progress = 20 + Math.round((i / updatePlan.steps.length) * 60);
|
progressCallback('Extracting new files...', 60, null, null, null);
|
||||||
progressCallback(`Downloading patch ${i + 1}/${updatePlan.steps.length} (${stepName})...`, progress, null, null, null);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const pwrFile = await downloadPWR(branch, stepName, progressCallback, cacheDir, false, step.url, step.size);
|
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 (!pwrFile) {
|
if (fs.existsSync(gameDir)) {
|
||||||
throw new Error(`Failed to download patch ${stepName}`);
|
console.log('Removing old game files...');
|
||||||
}
|
let retries = 3;
|
||||||
|
while (retries > 0) {
|
||||||
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 {
|
||||||
if (fs.existsSync(pwrFile)) {
|
fs.rmSync(gameDir, { recursive: true, force: true });
|
||||||
fs.unlinkSync(pwrFile);
|
break;
|
||||||
}
|
} catch (err) {
|
||||||
} catch (delErr) {
|
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
|
||||||
console.warn('[UpdateGameFiles] Failed to delete PWR from cache:', delErr.message);
|
retries--;
|
||||||
}
|
console.log(`[UpdateGameFiles] Removal failed with ${err.code}, retrying in 1s... (${retries} retries left)`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
// Save intermediate version so we can resume if interrupted
|
} else {
|
||||||
saveVersionClient(`v${step.to}`);
|
throw err;
|
||||||
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);
|
||||||
|
|
||||||
@@ -920,8 +818,7 @@ 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, expectedSize = null) {
|
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -930,20 +827,27 @@ function validatePWRFile(filePath, expectedSize = null) {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate against expected size if known (reject if < 99% of expected)
|
// Check if file is under 1.5 GB (incomplete download)
|
||||||
if (expectedSize && stats.size < expectedSize * 0.99) {
|
if (sizeInMB < 1500) {
|
||||||
const expectedMB = expectedSize / 1024 / 1024;
|
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
|
||||||
console.log(`[PWR Validation] File truncated: ${sizeInMB.toFixed(2)} MB, expected ${expectedMB.toFixed(2)} MB`);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[PWR Validation] File size: ${sizeInMB.toFixed(2)} MB - OK`);
|
// 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);
|
||||||
|
|||||||
@@ -2,248 +2,40 @@ 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');
|
||||||
|
|
||||||
// Patches CDN via auth server redirect gateway (allows instant CDN switching)
|
const BASE_PATCH_URL = 'https://game-patches.hytale.com/patches';
|
||||||
const AUTH_DOMAIN = process.env.HYTALE_AUTH_DOMAIN || 'auth.sanasol.ws';
|
const MANIFEST_API = 'https://files.hytalef2p.com/api/patch_manifest';
|
||||||
const MIRROR_BASE_URL = `https://${AUTH_DOMAIN}/patches`;
|
|
||||||
const MIRROR_MANIFEST_URL = `${MIRROR_BASE_URL}/manifest.json`;
|
|
||||||
|
|
||||||
// Fallback: latest known build number if manifest is unreachable
|
|
||||||
const FALLBACK_LATEST_BUILD = 11;
|
|
||||||
|
|
||||||
let manifestCache = null;
|
|
||||||
let manifestCacheTime = 0;
|
|
||||||
const MANIFEST_CACHE_DURATION = 60000; // 1 minute
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the mirror manifest from MEGA S4
|
|
||||||
*/
|
|
||||||
async function fetchMirrorManifest() {
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
if (manifestCache && (now - manifestCacheTime) < MANIFEST_CACHE_DURATION) {
|
|
||||||
console.log('[Mirror] Using cached manifest');
|
|
||||||
return manifestCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('[Mirror] Fetching manifest from:', MIRROR_MANIFEST_URL);
|
|
||||||
const response = await axios.get(MIRROR_MANIFEST_URL, {
|
|
||||||
timeout: 15000,
|
|
||||||
headers: { 'User-Agent': 'Hytale-F2P-Launcher' }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data && response.data.files) {
|
|
||||||
manifestCache = response.data;
|
|
||||||
manifestCacheTime = now;
|
|
||||||
console.log('[Mirror] Manifest fetched successfully');
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
throw new Error('Invalid manifest structure');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Mirror] Error fetching manifest:', error.message);
|
|
||||||
if (manifestCache) {
|
|
||||||
console.log('[Mirror] Using expired cache');
|
|
||||||
return manifestCache;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse manifest to get available patches for current platform
|
|
||||||
* Returns array of { from, to, key, size }
|
|
||||||
*/
|
|
||||||
function getPlatformPatches(manifest, branch = 'release') {
|
|
||||||
const os = getOS();
|
|
||||||
const arch = getArch();
|
|
||||||
const prefix = `${os}/${arch}/${branch}/`;
|
|
||||||
const patches = [];
|
|
||||||
|
|
||||||
for (const [key, info] of Object.entries(manifest.files)) {
|
|
||||||
if (key.startsWith(prefix) && key.endsWith('.pwr')) {
|
|
||||||
const filename = key.slice(prefix.length, -4); // e.g., "0_to_11"
|
|
||||||
const match = filename.match(/^(\d+)_to_(\d+)$/);
|
|
||||||
if (match) {
|
|
||||||
patches.push({
|
|
||||||
from: parseInt(match[1]),
|
|
||||||
to: parseInt(match[2]),
|
|
||||||
key,
|
|
||||||
size: info.size
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return patches;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find optimal patch path using BFS with download size minimization
|
|
||||||
* Returns array of { from, to, url, size, key } steps, or null if no path found
|
|
||||||
*/
|
|
||||||
function findOptimalPatchPath(currentBuild, targetBuild, patches) {
|
|
||||||
if (currentBuild >= targetBuild) return [];
|
|
||||||
|
|
||||||
const edges = {};
|
|
||||||
for (const patch of patches) {
|
|
||||||
if (!edges[patch.from]) edges[patch.from] = [];
|
|
||||||
edges[patch.from].push(patch);
|
|
||||||
}
|
|
||||||
|
|
||||||
const queue = [{ build: currentBuild, path: [], totalSize: 0 }];
|
|
||||||
let bestPath = null;
|
|
||||||
let bestSize = Infinity;
|
|
||||||
|
|
||||||
while (queue.length > 0) {
|
|
||||||
const { build, path, totalSize } = queue.shift();
|
|
||||||
|
|
||||||
if (build === targetBuild) {
|
|
||||||
if (totalSize < bestSize) {
|
|
||||||
bestPath = path;
|
|
||||||
bestSize = totalSize;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalSize >= bestSize) continue;
|
|
||||||
|
|
||||||
const nextEdges = edges[build] || [];
|
|
||||||
for (const edge of nextEdges) {
|
|
||||||
if (edge.to <= build || edge.to > targetBuild) continue;
|
|
||||||
if (path.some(p => p.to === edge.to)) continue;
|
|
||||||
|
|
||||||
queue.push({
|
|
||||||
build: edge.to,
|
|
||||||
path: [...path, {
|
|
||||||
from: edge.from,
|
|
||||||
to: edge.to,
|
|
||||||
url: `${MIRROR_BASE_URL}/${edge.key}`,
|
|
||||||
size: edge.size,
|
|
||||||
key: edge.key
|
|
||||||
}],
|
|
||||||
totalSize: totalSize + edge.size
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the optimal update plan from currentBuild to targetBuild
|
|
||||||
* Returns { steps: [{from, to, url, size}], totalSize, isFullInstall }
|
|
||||||
*/
|
|
||||||
async function getUpdatePlan(currentBuild, targetBuild, branch = 'release') {
|
|
||||||
const manifest = await fetchMirrorManifest();
|
|
||||||
const patches = getPlatformPatches(manifest, branch);
|
|
||||||
|
|
||||||
// Try optimal path
|
|
||||||
const steps = findOptimalPatchPath(currentBuild, targetBuild, patches);
|
|
||||||
|
|
||||||
if (steps && steps.length > 0) {
|
|
||||||
const totalSize = steps.reduce((sum, s) => sum + s.size, 0);
|
|
||||||
console.log(`[Mirror] Update plan: ${steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')} (${(totalSize / 1024 / 1024).toFixed(0)} MB)`);
|
|
||||||
return { steps, totalSize, isFullInstall: steps.length === 1 && steps[0].from === 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: full install 0 -> target
|
|
||||||
const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
|
|
||||||
if (fullPatch) {
|
|
||||||
const step = {
|
|
||||||
from: 0,
|
|
||||||
to: targetBuild,
|
|
||||||
url: `${MIRROR_BASE_URL}/${fullPatch.key}`,
|
|
||||||
size: fullPatch.size,
|
|
||||||
key: fullPatch.key
|
|
||||||
};
|
|
||||||
console.log(`[Mirror] Full install: 0\u2192${targetBuild} (${(fullPatch.size / 1024 / 1024).toFixed(0)} MB)`);
|
|
||||||
return { steps: [step], totalSize: fullPatch.size, isFullInstall: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`No patch path found from build ${currentBuild} to ${targetBuild} for ${getOS()}/${getArch()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getLatestClientVersion(branch = 'release') {
|
async function getLatestClientVersion(branch = 'release') {
|
||||||
try {
|
try {
|
||||||
console.log(`[Mirror] Fetching latest client version (branch: ${branch})...`);
|
console.log(`Fetching latest client version from API (branch: ${branch})...`);
|
||||||
const manifest = await fetchMirrorManifest();
|
const response = await smartRequest(`https://files.hytalef2p.com/api/version_client?branch=${branch}`, {
|
||||||
const patches = getPlatformPatches(manifest, branch);
|
timeout: 40000,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Hytale-F2P-Launcher'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (patches.length === 0) {
|
if (response.data && response.data.client_version) {
|
||||||
console.log(`[Mirror] No patches for branch '${branch}', using fallback`);
|
const version = response.data.client_version;
|
||||||
return `v${FALLBACK_LATEST_BUILD}`;
|
console.log(`Latest client version for ${branch}: ${version}`);
|
||||||
}
|
return version;
|
||||||
|
|
||||||
const latestBuild = Math.max(...patches.map(p => p.to));
|
|
||||||
console.log(`[Mirror] Latest client version: v${latestBuild}`);
|
|
||||||
return `v${latestBuild}`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Mirror] Error:', error.message);
|
|
||||||
return `v${FALLBACK_LATEST_BUILD}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get PWR download URL for fresh install (0 -> target)
|
|
||||||
* Backward-compatible with old getPWRUrlFromNewAPI signature
|
|
||||||
* Checks mirror first, then constructs URL for the branch
|
|
||||||
*/
|
|
||||||
async function getPWRUrl(branch = 'release', version = 'v11') {
|
|
||||||
const targetBuild = extractVersionNumber(version);
|
|
||||||
const os = getOS();
|
|
||||||
const arch = getArch();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const manifest = await fetchMirrorManifest();
|
|
||||||
const patches = getPlatformPatches(manifest, branch);
|
|
||||||
const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
|
|
||||||
|
|
||||||
if (fullPatch) {
|
|
||||||
const url = `${MIRROR_BASE_URL}/${fullPatch.key}`;
|
|
||||||
console.log(`[Mirror] PWR URL: ${url}`);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patches.length > 0) {
|
|
||||||
// Branch exists in mirror but no full patch for this target - construct URL
|
|
||||||
console.log(`[Mirror] No 0->${targetBuild} patch found, constructing URL`);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`[Mirror] Branch '${branch}' not in mirror, constructing URL`);
|
console.log('Warning: Invalid API response, falling back to latest known version (7.pwr)');
|
||||||
|
return '7.pwr';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Mirror] Error getting PWR URL:', error.message);
|
console.error('Error fetching client version:', error.message);
|
||||||
|
console.log('Warning: API unavailable, falling back to latest known version (7.pwr)');
|
||||||
|
return '7.pwr';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct mirror URL (will work if patch was uploaded but manifest is stale)
|
|
||||||
return `${MIRROR_BASE_URL}/${os}/${arch}/${branch}/0_to_${targetBuild}.pwr`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backward-compatible alias
|
|
||||||
const getPWRUrlFromNewAPI = getPWRUrl;
|
|
||||||
|
|
||||||
// Utility function to extract version number
|
|
||||||
// Supports: "7.pwr", "v8", "v8-windows-amd64.pwr", "5_to_10", etc.
|
|
||||||
function extractVersionNumber(version) {
|
|
||||||
if (!version) return 0;
|
|
||||||
|
|
||||||
// New format: "v8" or "v8-xxx.pwr"
|
|
||||||
const vMatch = version.match(/v(\d+)/);
|
|
||||||
if (vMatch) return parseInt(vMatch[1]);
|
|
||||||
|
|
||||||
// Old format: "7.pwr"
|
|
||||||
const pwrMatch = version.match(/(\d+)\.pwr/);
|
|
||||||
if (pwrMatch) return parseInt(pwrMatch[1]);
|
|
||||||
|
|
||||||
// Fallback
|
|
||||||
const num = parseInt(version);
|
|
||||||
return isNaN(num) ? 0 : num;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildArchiveUrl(buildNumber, branch = 'release') {
|
function buildArchiveUrl(buildNumber, branch = 'release') {
|
||||||
const os = getOS();
|
const os = getOS();
|
||||||
const arch = getArch();
|
const arch = getArch();
|
||||||
return `${MIRROR_BASE_URL}/${os}/${arch}/${branch}/0_to_${buildNumber}.pwr`;
|
return `${BASE_PATCH_URL}/${os}/${arch}/${branch}/0/${buildNumber}.pwr`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkArchiveExists(buildNumber, branch = 'release') {
|
async function checkArchiveExists(buildNumber, branch = 'release') {
|
||||||
@@ -251,56 +43,91 @@ 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 {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function discoverAvailableVersions(latestKnown, branch = 'release') {
|
async function discoverAvailableVersions(latestKnown, branch = 'release', maxProbe = 50) {
|
||||||
|
const available = [];
|
||||||
|
const latest = parseInt(latestKnown.replace('.pwr', ''));
|
||||||
|
|
||||||
|
for (let i = latest; i >= Math.max(1, latest - maxProbe); i--) {
|
||||||
|
const exists = await checkArchiveExists(i, branch);
|
||||||
|
if (exists) {
|
||||||
|
available.push(`${i}.pwr`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPatchManifest(branch = 'release') {
|
||||||
try {
|
try {
|
||||||
const manifest = await fetchMirrorManifest();
|
const os = getOS();
|
||||||
const patches = getPlatformPatches(manifest, branch);
|
const arch = getArch();
|
||||||
const versions = [...new Set(patches.map(p => p.to))].sort((a, b) => b - a);
|
const response = await smartRequest(`${MANIFEST_API}?branch=${branch}&os=${os}&arch=${arch}`, {
|
||||||
return versions.map(v => `${v}.pwr`);
|
timeout: 10000
|
||||||
} catch {
|
});
|
||||||
return [];
|
return response.data.patches || {};
|
||||||
|
} 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 = parseInt(targetVersion.replace('.pwr', ''));
|
||||||
const fullUrl = buildArchiveUrl(buildNumber, branch);
|
const previousBuild = buildNumber - 1;
|
||||||
|
|
||||||
|
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,
|
fullUrl: patchInfo?.original_url || buildArchiveUrl(buildNumber, branch),
|
||||||
differentialUrl: null,
|
differentialUrl: patchInfo?.patch_url || null,
|
||||||
checksum: null,
|
checksum: patchInfo?.patch_hash || null,
|
||||||
sourceVersion: null,
|
sourceVersion: patchInfo?.from ? `${patchInfo.from}.pwr` : (previousBuild > 0 ? `${previousBuild}.pwr` : null),
|
||||||
isDifferential: false,
|
isDifferential: !!patchInfo?.proper_patch,
|
||||||
releaseNotes: null
|
releaseNotes: patchInfo?.patch_note || null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function canUseDifferentialUpdate() {
|
function canUseDifferentialUpdate(currentVersion, targetDetails) {
|
||||||
// Differential updates are now handled via getUpdatePlan()
|
if (!targetDetails) return false;
|
||||||
return false;
|
if (!targetDetails.differentialUrl) return false;
|
||||||
|
if (!targetDetails.isDifferential) return false;
|
||||||
|
|
||||||
|
if (!currentVersion) return false;
|
||||||
|
|
||||||
|
const currentBuild = parseInt(currentVersion.replace('.pwr', ''));
|
||||||
|
const expectedSource = parseInt(targetDetails.sourceVersion?.replace('.pwr', '') || '0');
|
||||||
|
|
||||||
|
return currentBuild === expectedSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
function needsIntermediatePatches(currentVersion, targetVersion) {
|
function needsIntermediatePatches(currentVersion, targetVersion) {
|
||||||
if (!currentVersion) return [];
|
if (!currentVersion) return [];
|
||||||
const current = extractVersionNumber(currentVersion);
|
|
||||||
const target = extractVersionNumber(targetVersion);
|
const current = parseInt(currentVersion.replace('.pwr', ''));
|
||||||
if (current >= target) return [];
|
const target = parseInt(targetVersion.replace('.pwr', ''));
|
||||||
return [targetVersion];
|
|
||||||
|
const intermediates = [];
|
||||||
|
for (let i = current + 1; i <= target; i++) {
|
||||||
|
intermediates.push(`${i}.pwr`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return intermediates;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function computeFileChecksum(filePath) {
|
async function computeFileChecksum(filePath) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const hash = crypto.createHash('sha256');
|
const hash = crypto.createHash('sha256');
|
||||||
const stream = fs.createReadStream(filePath);
|
const stream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
stream.on('data', data => hash.update(data));
|
stream.on('data', data => hash.update(data));
|
||||||
stream.on('end', () => resolve(hash.digest('hex')));
|
stream.on('end', () => resolve(hash.digest('hex')));
|
||||||
stream.on('error', reject);
|
stream.on('error', reject);
|
||||||
@@ -309,6 +136,7 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -317,7 +145,7 @@ function getInstalledClientVersion() {
|
|||||||
try {
|
try {
|
||||||
const { loadVersionClient } = require('../core/config');
|
const { loadVersionClient } = require('../core/config');
|
||||||
return loadVersionClient();
|
return loadVersionClient();
|
||||||
} catch {
|
} catch (err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,13 +160,5 @@ module.exports = {
|
|||||||
needsIntermediatePatches,
|
needsIntermediatePatches,
|
||||||
computeFileChecksum,
|
computeFileChecksum,
|
||||||
validateChecksum,
|
validateChecksum,
|
||||||
getInstalledClientVersion,
|
getInstalledClientVersion
|
||||||
fetchMirrorManifest,
|
|
||||||
getPWRUrl,
|
|
||||||
getPWRUrlFromNewAPI,
|
|
||||||
getUpdatePlan,
|
|
||||||
extractVersionNumber,
|
|
||||||
getPlatformPatches,
|
|
||||||
findOptimalPatchPath,
|
|
||||||
MIRROR_BASE_URL
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
provider: generic
|
provider: github
|
||||||
url: https://git.sanhost.net/sanasol/hytale-f2p/releases/download/latest
|
owner: amiayweb # Change to your own GitHub username
|
||||||
|
repo: Hytale-F2P
|
||||||
|
|||||||
10
main.js
10
main.js
@@ -89,7 +89,7 @@ function setDiscordActivity() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Discord',
|
label: 'Discord',
|
||||||
url: 'https://discord.gg/Fhbb9Yk5WW'
|
url: 'https://discord.gg/hf2pdc'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -627,7 +627,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
|||||||
console.log('[Main] Processing Butler error with retry context');
|
console.log('[Main] Processing Butler error with retry context');
|
||||||
errorData.retryData = {
|
errorData.retryData = {
|
||||||
branch: error.branch || 'release',
|
branch: error.branch || 'release',
|
||||||
fileName: error.fileName || 'v8',
|
fileName: error.fileName || '7.pwr',
|
||||||
cacheDir: error.cacheDir
|
cacheDir: error.cacheDir
|
||||||
};
|
};
|
||||||
errorData.canRetry = error.canRetry !== false;
|
errorData.canRetry = error.canRetry !== false;
|
||||||
@@ -647,7 +647,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
|||||||
console.log('[Main] Processing generic error, creating default retry data');
|
console.log('[Main] Processing generic error, creating default retry data');
|
||||||
errorData.retryData = {
|
errorData.retryData = {
|
||||||
branch: 'release',
|
branch: 'release',
|
||||||
fileName: 'v8'
|
fileName: '7.pwr'
|
||||||
};
|
};
|
||||||
// For generic errors, assume it's retryable unless specified
|
// For generic errors, assume it's retryable unless specified
|
||||||
errorData.canRetry = error.canRetry !== false;
|
errorData.canRetry = error.canRetry !== false;
|
||||||
@@ -887,7 +887,7 @@ ipcMain.handle('retry-download', async (event, retryData) => {
|
|||||||
console.log('[IPC] Invalid retry data, using PWR defaults');
|
console.log('[IPC] Invalid retry data, using PWR defaults');
|
||||||
retryData = {
|
retryData = {
|
||||||
branch: 'release',
|
branch: 'release',
|
||||||
fileName: 'v8'
|
fileName: '7.pwr'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,7 +921,7 @@ ipcMain.handle('retry-download', async (event, retryData) => {
|
|||||||
} :
|
} :
|
||||||
{
|
{
|
||||||
branch: retryData?.branch || 'release',
|
branch: retryData?.branch || 'release',
|
||||||
fileName: retryData?.fileName || 'v8',
|
fileName: retryData?.fileName || '7.pwr',
|
||||||
cacheDir: retryData?.cacheDir
|
cacheDir: retryData?.cacheDir
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.3.3",
|
"version": "2.2.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,8 +118,9 @@
|
|||||||
"createStartMenuShortcut": true
|
"createStartMenuShortcut": true
|
||||||
},
|
},
|
||||||
"publish": {
|
"publish": {
|
||||||
"provider": "generic",
|
"provider": "github",
|
||||||
"url": "https://git.sanhost.net/sanasol/hytale-f2p/releases/download/latest"
|
"owner": "amiayweb",
|
||||||
|
"repo": "Hytale-F2P"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user