mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 19:51:47 -03:00
Compare commits
23 Commits
refactor/b
...
e7a033932f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7a033932f | ||
|
|
11c6d40dfe | ||
|
|
0dafb17c7b | ||
|
|
66112f15b2 | ||
|
|
0a71fdac8c | ||
|
|
4b9eae215b | ||
|
|
1510eceb0f | ||
|
|
c4b5368538 | ||
|
|
e0ebf137fc | ||
|
|
5241a502e5 | ||
|
|
3e7c7ccff3 | ||
|
|
89d09f032f | ||
|
|
8bdb78d1e2 | ||
|
|
e9e66dbca7 | ||
|
|
82f1dd2739 | ||
|
|
4502c11bd0 | ||
|
|
bcc7476322 | ||
|
|
ae6a7db80a | ||
|
|
48395fbff3 | ||
|
|
aae90a72e8 | ||
|
|
a6c61aef68 | ||
|
|
31653a37a7 | ||
|
|
1cb08f029a |
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/hf2pdc). All complaints will be reviewed and investigated promptly and fairly.
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Discord Server, message Founders/Devs](https://discord.gg/Fhbb9Yk5WW). All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||||
|
|
||||||
|
|||||||
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/gME8rUy3MB)!
|
**Need a quick assistance?** Please Open-A-Ticket in our [Discord Server](https://discord.gg/Fhbb9Yk5WW)!
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: question
|
id: question
|
||||||
|
|||||||
28
.github/workflows/release.yml
vendored
28
.github/workflows/release.yml
vendored
@@ -6,6 +6,12 @@ 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:
|
create-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -13,7 +19,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Create Draft Release
|
- name: Create Draft Release
|
||||||
run: |
|
run: |
|
||||||
curl -s -X POST "https://git.sanhost.net/api/v1/repos/${GITHUB_REPOSITORY}/releases" \
|
curl -s -X POST "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases" \
|
||||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"tag_name\":\"${{ github.ref_name }}\",\"name\":\"${{ github.ref_name }}\",\"body\":\"Release ${{ github.ref_name }}\",\"draft\":true,\"prerelease\":false}" \
|
-d "{\"tag_name\":\"${{ github.ref_name }}\",\"name\":\"${{ github.ref_name }}\",\"body\":\"Release ${{ github.ref_name }}\",\"draft\":true,\"prerelease\":false}" \
|
||||||
@@ -40,16 +46,16 @@ jobs:
|
|||||||
- run: npm ci
|
- run: npm ci
|
||||||
|
|
||||||
- name: Build Windows Packages
|
- name: Build Windows Packages
|
||||||
run: npx electron-builder --win --publish never
|
run: npx electron-builder --win --publish never --config.npmRebuild=false
|
||||||
|
|
||||||
- name: Upload to Release
|
- name: Upload to Release
|
||||||
run: |
|
run: |
|
||||||
RELEASE_ID=$(curl -s "https://git.sanhost.net/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
|
RELEASE_ID=$(curl -s "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
|
||||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
||||||
for file in dist/*.exe dist/*.exe.blockmap dist/latest.yml; do
|
for file in dist/*.exe dist/*.exe.blockmap dist/latest.yml; do
|
||||||
[ -f "$file" ] || continue
|
[ -f "$file" ] || continue
|
||||||
echo "Uploading $file..."
|
echo "Uploading $file..."
|
||||||
curl -s -X POST "https://git.sanhost.net/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=$(basename $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 }}" \
|
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||||
-F "attachment=@${file}" || echo "Failed to upload $file"
|
-F "attachment=@${file}" || echo "Failed to upload $file"
|
||||||
done
|
done
|
||||||
@@ -75,19 +81,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload to Release
|
- name: Upload to Release
|
||||||
run: |
|
run: |
|
||||||
RELEASE_ID=$(curl -s "https://git.sanhost.net/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
|
RELEASE_ID=$(curl -s "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
|
||||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
||||||
for file in dist/*.dmg dist/*.zip dist/*.blockmap dist/latest-mac.yml; do
|
for file in dist/*.dmg dist/*.zip dist/*.blockmap dist/latest-mac.yml; do
|
||||||
[ -f "$file" ] || continue
|
[ -f "$file" ] || continue
|
||||||
echo "Uploading $file..."
|
echo "Uploading $file..."
|
||||||
curl -s -X POST "https://git.sanhost.net/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=$(basename $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 }}" \
|
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||||
-F "attachment=@${file}" || echo "Failed to upload $file"
|
-F "attachment=@${file}" || echo "Failed to upload $file"
|
||||||
done
|
done
|
||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
needs: [create-release]
|
|
||||||
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
|
||||||
@@ -101,16 +107,16 @@ jobs:
|
|||||||
- run: npm ci
|
- run: npm ci
|
||||||
|
|
||||||
- name: Build Linux Packages
|
- name: Build Linux Packages
|
||||||
run: npx electron-builder --linux AppImage deb rpm --publish never
|
run: npx electron-builder --linux AppImage deb rpm pacman --publish never
|
||||||
|
|
||||||
- name: Upload to Release
|
- name: Upload to Release
|
||||||
run: |
|
run: |
|
||||||
RELEASE_ID=$(curl -s "https://git.sanhost.net/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
|
RELEASE_ID=$(curl -s "${FORGEJO_API}/repos/${GITHUB_REPOSITORY}/releases/tags/${{ github.ref_name }}" \
|
||||||
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" | python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')
|
||||||
for file in dist/*.AppImage dist/*.AppImage.blockmap dist/*.deb dist/*.rpm dist/latest-linux.yml; do
|
for file in dist/*.AppImage dist/*.AppImage.blockmap dist/*.deb dist/*.rpm dist/*.pacman dist/latest-linux.yml; do
|
||||||
[ -f "$file" ] || continue
|
[ -f "$file" ] || continue
|
||||||
echo "Uploading $file..."
|
echo "Uploading $file..."
|
||||||
curl -s -X POST "https://git.sanhost.net/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=$(basename $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 }}" \
|
-H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" \
|
||||||
-F "attachment=@${file}" || echo "Failed to upload $file"
|
-F "attachment=@${file}" || echo "Failed to upload $file"
|
||||||
done
|
done
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,6 +17,9 @@ dist/
|
|||||||
# Project Specific: Downloaded patcher (from hytale-auth-server)
|
# Project Specific: Downloaded patcher (from hytale-auth-server)
|
||||||
backend/patcher/
|
backend/patcher/
|
||||||
|
|
||||||
|
# Private docs (local only)
|
||||||
|
docs/PATCH_CDN_INFRASTRUCTURE.md
|
||||||
|
|
||||||
# macOS Specific
|
# macOS Specific
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.zst.DS_Store
|
*.zst.DS_Store
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ window.closeDiscordPopup = function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
window.joinDiscord = async function() {
|
window.joinDiscord = async function() {
|
||||||
await window.electronAPI?.openExternal('https://discord.gg/hf2pdc');
|
await window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.electronAPI?.saveConfig({ discordPopup: true });
|
await window.electronAPI?.saveConfig({ discordPopup: true });
|
||||||
|
|||||||
@@ -1103,7 +1103,7 @@ function getRetryContextMessage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.openDiscordExternal = function() {
|
window.openDiscordExternal = function() {
|
||||||
window.electronAPI?.openExternal('https://discord.gg/hf2pdc');
|
window.electronAPI?.openExternal('https://discord.gg/Fhbb9Yk5WW');
|
||||||
};
|
};
|
||||||
|
|
||||||
window.toggleMaximize = toggleMaximize;
|
window.toggleMaximize = toggleMaximize;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
### ⚠️ **WARNING: READ [QUICK START](#-quick-start) before Downloading & Installing the Launcher!** ⚠️
|
### ⚠️ **WARNING: READ [QUICK START](#-quick-start) before Downloading & Installing the Launcher!** ⚠️
|
||||||
|
|
||||||
#### 🛑 **Found a problem? [Join the HF2P Discord](https://discord.gg/hf2pdc) and head to `#-⚠️-community-help`** 🛑
|
#### 🛑 **Found a problem? [Join the HF2P Discord](https://discord.gg/Fhbb9Yk5WW) and head to `#-⚠️-community-help`** 🛑
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
👍 If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> ☕<br>
|
👍 If you like the project, <b>feel free to support us via Buy Me a Coffee!</b> ☕<br>
|
||||||
@@ -455,7 +455,7 @@ See [BUILD.md](docs/BUILD.md) for comprehensive build instructions.
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
**Questions? Ads? Collaboration? Endorsement? Other business-related?**
|
**Questions? Ads? Collaboration? Endorsement? Other business-related?**
|
||||||
Message the founders at https://discord.gg/hf2pdc
|
Message the founders at https://discord.gg/Fhbb9Yk5WW
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup.
|
Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup.
|
||||||
|
|
||||||
### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/hf2pdc**
|
### **DOWNLOAD SERVER FILES (JAR/RAR/SCRIPTS) HERE: https://discord.gg/Fhbb9Yk5WW**
|
||||||
|
|
||||||
**Table of Contents**
|
**Table of Contents**
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Hytale F2P Launcher - Troubleshooting Guide
|
# Hytale F2P Launcher - Troubleshooting Guide
|
||||||
|
|
||||||
This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [Discord](https://discord.gg/gME8rUy3MB).
|
This guide covers common issues and their solutions. If your issue isn't listed here, please check [existing issues](https://github.com/amiayweb/Hytale-F2P/issues) or join our [Discord](https://discord.gg/Fhbb9Yk5WW).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -437,7 +437,7 @@ Game sessions have a 10-hour TTL. This is by design for security.
|
|||||||
If your issue isn't resolved by this guide:
|
If your issue isn't resolved by this guide:
|
||||||
|
|
||||||
1. **Check existing issues:** [GitHub Issues](https://github.com/amiayweb/Hytale-F2P/issues)
|
1. **Check existing issues:** [GitHub Issues](https://github.com/amiayweb/Hytale-F2P/issues)
|
||||||
2. **Join Discord:** [discord.gg/gME8rUy3MB](https://discord.gg/gME8rUy3MB)
|
2. **Join Discord:** [discord.gg/Fhbb9Yk5WW](https://discord.gg/Fhbb9Yk5WW)
|
||||||
3. **Open a new issue** with:
|
3. **Open a new issue** with:
|
||||||
- Your operating system and version
|
- Your operating system and version
|
||||||
- Launcher version
|
- Launcher version
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const FORCE_CLEAN_INSTALL_VERSION = false;
|
const FORCE_CLEAN_INSTALL_VERSION = false;
|
||||||
const CLEAN_INSTALL_TEST_VERSION = '4.pwr';
|
const CLEAN_INSTALL_TEST_VERSION = 'v4';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
FORCE_CLEAN_INSTALL_VERSION,
|
FORCE_CLEAN_INSTALL_VERSION,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const path = require('path');
|
|||||||
const { execFile } = require('child_process');
|
const { execFile } = require('child_process');
|
||||||
const { downloadFile, retryDownload } = require('../utils/fileManager');
|
const { downloadFile, retryDownload } = require('../utils/fileManager');
|
||||||
const { getOS, getArch } = require('../utils/platformUtils');
|
const { getOS, getArch } = require('../utils/platformUtils');
|
||||||
const { validateChecksum, extractVersionDetails, canUseDifferentialUpdate, needsIntermediatePatches, getInstalledClientVersion } = require('../services/versionManager');
|
const { validateChecksum, extractVersionDetails, getInstalledClientVersion, getUpdatePlan, extractVersionNumber } = require('../services/versionManager');
|
||||||
const { installButler } = require('./butlerManager');
|
const { installButler } = require('./butlerManager');
|
||||||
const { GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
const { GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
||||||
const { saveVersionClient } = require('../core/config');
|
const { saveVersionClient } = require('../core/config');
|
||||||
@@ -156,15 +156,15 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
|
|||||||
console.log(`Initiating intelligent update to version ${targetVersion}`);
|
console.log(`Initiating intelligent update to version ${targetVersion}`);
|
||||||
|
|
||||||
const currentVersion = getInstalledClientVersion();
|
const currentVersion = getInstalledClientVersion();
|
||||||
console.log(`Current version: ${currentVersion || 'none (clean install)'}`);
|
const currentBuild = extractVersionNumber(currentVersion) || 0;
|
||||||
console.log(`Target version: ${targetVersion}`);
|
const targetBuild = extractVersionNumber(targetVersion);
|
||||||
console.log(`Branch: ${branch}`);
|
console.log(`Current build: ${currentBuild}, Target build: ${targetBuild}, Branch: ${branch}`);
|
||||||
|
|
||||||
|
// For non-release branches, always do full install
|
||||||
if (branch !== 'release') {
|
if (branch !== 'release') {
|
||||||
console.log(`Pre-release branch detected - forcing full archive download`);
|
console.log('Pre-release branch detected - forcing full archive download');
|
||||||
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
||||||
const archiveName = path.basename(versionDetails.fullUrl);
|
const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
|
||||||
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Downloading full game archive (pre-release)...', 0, null, null, null);
|
progressCallback('Downloading full game archive (pre-release)...', 0, null, null, null);
|
||||||
@@ -177,14 +177,14 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentVersion) {
|
// Clean install (no current version)
|
||||||
|
if (currentBuild === 0) {
|
||||||
console.log('No existing installation detected - downloading full archive');
|
console.log('No existing installation detected - downloading full archive');
|
||||||
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
||||||
const archiveName = path.basename(versionDetails.fullUrl);
|
const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
|
||||||
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Downloading full game archive (first install - v${targetVersion})...`, 0, null, null, null);
|
progressCallback(`Downloading full game archive (first install - v${targetBuild})...`, 0, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||||
@@ -194,59 +194,67 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const patchesToApply = needsIntermediatePatches(currentVersion, targetVersion);
|
// Already at target
|
||||||
|
if (currentBuild >= targetBuild) {
|
||||||
if (patchesToApply.length === 0) {
|
console.log('Already at target version or newer');
|
||||||
console.log('Already at target version or invalid version sequence');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Applying ${patchesToApply.length} differential patch(es): ${patchesToApply.join(' -> ')}`);
|
// Use mirror's update plan for optimal patch routing
|
||||||
|
try {
|
||||||
|
const plan = await getUpdatePlan(currentBuild, targetBuild, branch);
|
||||||
|
|
||||||
for (let i = 0; i < patchesToApply.length; i++) {
|
console.log(`Applying ${plan.steps.length} patch(es): ${plan.steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')}`);
|
||||||
const patchVersion = patchesToApply[i];
|
|
||||||
const versionDetails = await extractVersionDetails(patchVersion, branch);
|
|
||||||
|
|
||||||
const canDifferential = canUseDifferentialUpdate(getInstalledClientVersion(), versionDetails);
|
for (let i = 0; i < plan.steps.length; i++) {
|
||||||
|
const step = plan.steps[i];
|
||||||
if (!canDifferential || !versionDetails.differentialUrl) {
|
const stepName = `${step.from}_to_${step.to}`;
|
||||||
console.log(`WARNING: Differential patch not available for ${patchVersion}, using full archive`);
|
const archivePath = path.join(cacheDir, `${branch}_${stepName}.pwr`);
|
||||||
const archiveName = path.basename(versionDetails.fullUrl);
|
const isDifferential = step.from !== 0;
|
||||||
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Downloading full archive for ${patchVersion} (${i + 1}/${patchesToApply.length})...`, 0, null, null, null);
|
progressCallback(`Downloading patch ${i + 1}/${plan.steps.length}: ${stepName}...`, 0, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
await acquireGameArchive(step.url, archivePath, null, progressCallback);
|
||||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
|
||||||
} else {
|
|
||||||
console.log(`Applying differential patch: ${versionDetails.sourceVersion} -> ${patchVersion}`);
|
|
||||||
const archiveName = path.basename(versionDetails.differentialUrl);
|
|
||||||
const archivePath = path.join(cacheDir, `${branch}_patch_${archiveName}`);
|
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Applying patch ${i + 1}/${patchesToApply.length}: ${patchVersion}...`, 0, null, null, null);
|
progressCallback(`Applying patch ${i + 1}/${plan.steps.length}: ${stepName}...`, 50, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
await acquireGameArchive(versionDetails.differentialUrl, archivePath, versionDetails.checksum, progressCallback);
|
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, isDifferential);
|
||||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, true);
|
|
||||||
|
|
||||||
|
// Clean up patch file
|
||||||
if (fs.existsSync(archivePath)) {
|
if (fs.existsSync(archivePath)) {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(archivePath);
|
fs.unlinkSync(archivePath);
|
||||||
console.log(`Cleaned up patch file: ${archiveName}`);
|
console.log(`Cleaned up: ${stepName}.pwr`);
|
||||||
} catch (cleanupErr) {
|
} catch (cleanupErr) {
|
||||||
console.warn(`Failed to cleanup patch file: ${cleanupErr.message}`);
|
console.warn(`Failed to cleanup: ${cleanupErr.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveVersionClient(`v${step.to}`);
|
||||||
|
console.log(`Patch ${stepName} applied (${i + 1}/${plan.steps.length})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveVersionClient(patchVersion);
|
console.log(`Update completed. Version ${targetVersion} is now installed.`);
|
||||||
console.log(`Patch ${patchVersion} applied successfully (${i + 1}/${patchesToApply.length})`);
|
} catch (planError) {
|
||||||
}
|
console.error('Update plan failed:', planError.message);
|
||||||
|
console.log('Falling back to full archive download');
|
||||||
|
|
||||||
console.log(`Update completed successfully. Version ${targetVersion} is now installed.`);
|
// Fallback: full install
|
||||||
|
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
||||||
|
const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(`Downloading full game archive (fallback)...`, 0, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||||
|
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||||
|
saveVersionClient(targetVersion);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureGameInstalled(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) {
|
async function ensureGameInstalled(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const { promisify } = require('util');
|
|||||||
const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursive, GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursive, GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
||||||
const { getOS, getArch } = require('../utils/platformUtils');
|
const { getOS, getArch } = require('../utils/platformUtils');
|
||||||
const { downloadFile, retryDownload, retryStalledDownload, MAX_AUTOMATIC_STALL_RETRIES } = require('../utils/fileManager');
|
const { downloadFile, retryDownload, retryStalledDownload, MAX_AUTOMATIC_STALL_RETRIES } = require('../utils/fileManager');
|
||||||
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
|
const { getLatestClientVersion, getInstalledClientVersion, getUpdatePlan, extractVersionNumber } = require('../services/versionManager');
|
||||||
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
|
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
|
||||||
const { installButler } = require('./butlerManager');
|
const { installButler } = require('./butlerManager');
|
||||||
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
||||||
@@ -64,7 +64,7 @@ async function safeRemoveDirectory(dirPath, maxRetries = 3) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback, cacheDir = CACHE_DIR, manualRetry = false, directUrl = null, expectedSize = null) {
|
||||||
const osName = getOS();
|
const osName = getOS();
|
||||||
const arch = getArch();
|
const arch = getArch();
|
||||||
|
|
||||||
@@ -72,28 +72,68 @@ async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallb
|
|||||||
throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.');
|
throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${branch}/0/${fileName}`;
|
let url;
|
||||||
const dest = path.join(cacheDir, `${branch}_${fileName}`);
|
|
||||||
|
|
||||||
// Check if file exists and validate it
|
if (directUrl) {
|
||||||
if (fs.existsSync(dest) && !manualRetry) {
|
url = directUrl;
|
||||||
console.log('PWR file found in cache:', dest);
|
console.log(`[DownloadPWR] Using direct URL: ${url}`);
|
||||||
|
} else {
|
||||||
// Validate file size (PWR files should be > 1MB and >= 1.5GB for complete downloads)
|
const { getPWRUrl } = require('../services/versionManager');
|
||||||
const stats = fs.statSync(dest);
|
try {
|
||||||
if (stats.size < 1024 * 1024) {
|
console.log(`[DownloadPWR] Fetching mirror URL for branch: ${branch}, version: ${fileName}`);
|
||||||
return false;
|
url = await getPWRUrl(branch, fileName);
|
||||||
}
|
console.log(`[DownloadPWR] Mirror URL: ${url}`);
|
||||||
|
} catch (error) {
|
||||||
// Check if file is under 1.5 GB (incomplete download)
|
console.error(`[DownloadPWR] Failed to get mirror URL: ${error.message}`);
|
||||||
const sizeInMB = stats.size / 1024 / 1024;
|
const { MIRROR_BASE_URL } = require('../services/versionManager');
|
||||||
if (sizeInMB < 1500) {
|
url = `${MIRROR_BASE_URL}/${osName}/${arch}/${branch}/0_to_${extractVersionNumber(fileName)}.pwr`;
|
||||||
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
|
console.log(`[DownloadPWR] Fallback URL: ${url}`);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Fetching PWR patch file:', url);
|
// Look up expected file size from manifest if not provided
|
||||||
|
if (!expectedSize) {
|
||||||
|
try {
|
||||||
|
const { fetchMirrorManifest } = require('../services/versionManager');
|
||||||
|
const manifest = await fetchMirrorManifest();
|
||||||
|
// Try to match: "0_to_11" format or "v11" format
|
||||||
|
const versionMatch = fileName.match(/^(\d+)_to_(\d+)$/);
|
||||||
|
let manifestKey;
|
||||||
|
if (versionMatch) {
|
||||||
|
manifestKey = `${osName}/${arch}/${branch}/${fileName}.pwr`;
|
||||||
|
} else {
|
||||||
|
const buildNum = extractVersionNumber(fileName);
|
||||||
|
manifestKey = `${osName}/${arch}/${branch}/0_to_${buildNum}.pwr`;
|
||||||
|
}
|
||||||
|
if (manifest.files[manifestKey]) {
|
||||||
|
expectedSize = manifest.files[manifestKey].size;
|
||||||
|
console.log(`[PWR] Expected size from manifest: ${(expectedSize / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[PWR] Could not fetch expected size from manifest: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dest = path.join(cacheDir, `${branch}_${fileName}.pwr`);
|
||||||
|
|
||||||
|
// Check if file exists and validate it
|
||||||
|
if (fs.existsSync(dest) && !manualRetry) {
|
||||||
|
const stats = fs.statSync(dest);
|
||||||
|
if (stats.size > 1024 * 1024) {
|
||||||
|
// Validate against expected size - reject if file is truncated (< 99% of expected)
|
||||||
|
if (expectedSize && stats.size < expectedSize * 0.99) {
|
||||||
|
console.log(`[PWR] Cached file truncated: ${(stats.size / 1024 / 1024).toFixed(2)} MB, expected ${(expectedSize / 1024 / 1024).toFixed(2)} MB. Deleting and re-downloading.`);
|
||||||
|
fs.unlinkSync(dest);
|
||||||
|
} else {
|
||||||
|
console.log(`[PWR] Using cached file: ${dest} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`[PWR] Cached file too small (${stats.size} bytes), re-downloading`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[DownloadPWR] Downloading from: ${url}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (manualRetry) {
|
if (manualRetry) {
|
||||||
@@ -119,7 +159,7 @@ async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallb
|
|||||||
const retryStats = fs.statSync(dest);
|
const retryStats = fs.statSync(dest);
|
||||||
console.log(`PWR file downloaded (auto-retry), size: ${(retryStats.size / 1024 / 1024).toFixed(2)} MB`);
|
console.log(`PWR file downloaded (auto-retry), size: ${(retryStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
if (!validatePWRFile(dest)) {
|
if (!validatePWRFile(dest, expectedSize)) {
|
||||||
console.log(`[PWR Validation] PWR file validation failed after auto-retry, deleting corrupted file: ${dest}`);
|
console.log(`[PWR Validation] PWR file validation failed after auto-retry, deleting corrupted file: ${dest}`);
|
||||||
fs.unlinkSync(dest);
|
fs.unlinkSync(dest);
|
||||||
throw new Error('Downloaded PWR file is corrupted or invalid after automatic retry. Please retry manually');
|
throw new Error('Downloaded PWR file is corrupted or invalid after automatic retry. Please retry manually');
|
||||||
@@ -170,7 +210,7 @@ async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallb
|
|||||||
const stats = fs.statSync(dest);
|
const stats = fs.statSync(dest);
|
||||||
console.log(`PWR file downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
console.log(`PWR file downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
if (!validatePWRFile(dest)) {
|
if (!validatePWRFile(dest, expectedSize)) {
|
||||||
console.log(`[PWR Validation] PWR file validation failed, deleting corrupted file: ${dest}`);
|
console.log(`[PWR Validation] PWR file validation failed, deleting corrupted file: ${dest}`);
|
||||||
fs.unlinkSync(dest);
|
fs.unlinkSync(dest);
|
||||||
throw new Error('Downloaded PWR file is corrupted or invalid. Please retry');
|
throw new Error('Downloaded PWR file is corrupted or invalid. Please retry');
|
||||||
@@ -188,7 +228,7 @@ async function retryPWRDownload(branch, fileName, progressCallback, cacheDir = C
|
|||||||
return await downloadPWR(branch, fileName, progressCallback, cacheDir, true);
|
return await downloadPWR(branch, fileName, progressCallback, cacheDir, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, branch = 'release', cacheDir = CACHE_DIR) {
|
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, branch = 'release', cacheDir = CACHE_DIR, skipExistingCheck = false) {
|
||||||
console.log(`[Butler] Starting PWR application with:`);
|
console.log(`[Butler] Starting PWR application with:`);
|
||||||
console.log(`[Butler] - PWR file: ${pwrFile}`);
|
console.log(`[Butler] - PWR file: ${pwrFile}`);
|
||||||
console.log(`[Butler] - Staging dir: ${path.join(gameDir, 'staging-temp')}`);
|
console.log(`[Butler] - Staging dir: ${path.join(gameDir, 'staging-temp')}`);
|
||||||
@@ -212,11 +252,12 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
|||||||
const gameLatest = gameDir;
|
const gameLatest = gameDir;
|
||||||
const stagingDir = path.join(gameLatest, 'staging-temp');
|
const stagingDir = path.join(gameLatest, 'staging-temp');
|
||||||
|
|
||||||
const clientPath = findClientPath(gameLatest);
|
if (!skipExistingCheck) {
|
||||||
|
const clientPath = findClientPath(gameLatest);
|
||||||
if (clientPath) {
|
if (clientPath) {
|
||||||
console.log('Game files detected, skipping patch installation.');
|
console.log('Game files detected, skipping patch installation.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate and prepare directories
|
// Validate and prepare directories
|
||||||
@@ -397,57 +438,118 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
}
|
}
|
||||||
console.log(`Updating game files to version: ${newVersion} (branch: ${branch})`);
|
console.log(`Updating game files to version: ${newVersion} (branch: ${branch})`);
|
||||||
|
|
||||||
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
|
// Determine update strategy: intermediate patches vs full reinstall
|
||||||
|
const currentVersion = loadVersionClient();
|
||||||
|
const currentBuild = extractVersionNumber(currentVersion) || 0;
|
||||||
|
const targetBuild = extractVersionNumber(newVersion);
|
||||||
|
|
||||||
if (fs.existsSync(tempUpdateDir)) {
|
let useIntermediatePatches = false;
|
||||||
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
let updatePlan = null;
|
||||||
}
|
|
||||||
fs.mkdirSync(tempUpdateDir, { recursive: true });
|
|
||||||
|
|
||||||
if (progressCallback) {
|
if (currentBuild > 0 && currentBuild < targetBuild) {
|
||||||
progressCallback('Downloading new game version...', 20, null, null, null);
|
try {
|
||||||
}
|
updatePlan = await getUpdatePlan(currentBuild, targetBuild, branch);
|
||||||
|
useIntermediatePatches = !updatePlan.isFullInstall;
|
||||||
const pwrFile = await downloadPWR(branch, newVersion, progressCallback, cacheDir);
|
if (useIntermediatePatches) {
|
||||||
|
const totalMB = (updatePlan.totalSize / 1024 / 1024).toFixed(0);
|
||||||
if (progressCallback) {
|
console.log(`[UpdateGameFiles] Using intermediate patches: ${updatePlan.steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')} (${totalMB} MB)`);
|
||||||
progressCallback('Extracting new files...', 60, null, null, null);
|
}
|
||||||
}
|
} catch (planError) {
|
||||||
|
console.warn('[UpdateGameFiles] Could not get update plan, falling back to full install:', planError.message);
|
||||||
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir);
|
|
||||||
// Delete PWR file from cache after successful update
|
|
||||||
try {
|
|
||||||
if (fs.existsSync(pwrFile)) {
|
|
||||||
fs.unlinkSync(pwrFile);
|
|
||||||
console.log('[UpdateGameFiles] PWR file deleted from cache after successful update:', pwrFile);
|
|
||||||
}
|
}
|
||||||
} catch (delErr) {
|
|
||||||
console.warn('[UpdateGameFiles] Failed to delete PWR file from cache:', delErr.message);
|
|
||||||
}
|
|
||||||
if (progressCallback) {
|
|
||||||
progressCallback('Replacing game files...', 80, null, null, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(gameDir)) {
|
if (useIntermediatePatches && updatePlan) {
|
||||||
console.log('Removing old game files...');
|
// Apply intermediate patches directly to game dir
|
||||||
let retries = 3;
|
for (let i = 0; i < updatePlan.steps.length; i++) {
|
||||||
while (retries > 0) {
|
const step = updatePlan.steps[i];
|
||||||
|
const stepName = `${step.from}_to_${step.to}`;
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
const progress = 20 + Math.round((i / updatePlan.steps.length) * 60);
|
||||||
|
progressCallback(`Downloading patch ${i + 1}/${updatePlan.steps.length} (${stepName})...`, progress, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pwrFile = await downloadPWR(branch, stepName, progressCallback, cacheDir, false, step.url, step.size);
|
||||||
|
|
||||||
|
if (!pwrFile) {
|
||||||
|
throw new Error(`Failed to download patch ${stepName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(`Applying patch ${i + 1}/${updatePlan.steps.length} (${stepName})...`, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await applyPWR(pwrFile, progressCallback, gameDir, toolsDir, branch, cacheDir, true);
|
||||||
|
|
||||||
|
// Clean up PWR file from cache
|
||||||
try {
|
try {
|
||||||
fs.rmSync(gameDir, { recursive: true, force: true });
|
if (fs.existsSync(pwrFile)) {
|
||||||
break;
|
fs.unlinkSync(pwrFile);
|
||||||
} catch (err) {
|
}
|
||||||
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
|
} catch (delErr) {
|
||||||
retries--;
|
console.warn('[UpdateGameFiles] Failed to delete PWR from cache:', delErr.message);
|
||||||
console.log(`[UpdateGameFiles] Removal failed with ${err.code}, retrying in 1s... (${retries} retries left)`);
|
}
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
} else {
|
// Save intermediate version so we can resume if interrupted
|
||||||
throw err;
|
saveVersionClient(`v${step.to}`);
|
||||||
|
console.log(`[UpdateGameFiles] Applied patch ${stepName} (${i + 1}/${updatePlan.steps.length})`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Full install: download 0->target, apply to temp dir, swap
|
||||||
|
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
|
||||||
|
|
||||||
|
if (fs.existsSync(tempUpdateDir)) {
|
||||||
|
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(tempUpdateDir, { recursive: true });
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback('Downloading new game version...', 20, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pwrFile = await downloadPWR(branch, newVersion, progressCallback, cacheDir);
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback('Extracting new files...', 60, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir, branch, cacheDir);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(pwrFile)) {
|
||||||
|
fs.unlinkSync(pwrFile);
|
||||||
|
console.log('[UpdateGameFiles] PWR file deleted from cache after successful update:', pwrFile);
|
||||||
|
}
|
||||||
|
} catch (delErr) {
|
||||||
|
console.warn('[UpdateGameFiles] Failed to delete PWR file from cache:', delErr.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback('Replacing game files...', 80, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(gameDir)) {
|
||||||
|
console.log('Removing old game files...');
|
||||||
|
let retries = 3;
|
||||||
|
while (retries > 0) {
|
||||||
|
try {
|
||||||
|
fs.rmSync(gameDir, { recursive: true, force: true });
|
||||||
|
break;
|
||||||
|
} catch (err) {
|
||||||
|
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
|
||||||
|
retries--;
|
||||||
|
console.log(`[UpdateGameFiles] Removal failed with ${err.code}, retrying in 1s... (${retries} retries left)`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fs.renameSync(tempUpdateDir, gameDir);
|
fs.renameSync(tempUpdateDir, gameDir);
|
||||||
|
}
|
||||||
|
|
||||||
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
|
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
|
||||||
console.log('HomePage.ui update result after update:', homeUIResult);
|
console.log('HomePage.ui update result after update:', homeUIResult);
|
||||||
@@ -818,7 +920,8 @@ function validateGameDirectory(gameDir, stagingDir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced PWR file validation
|
// Enhanced PWR file validation
|
||||||
function validatePWRFile(filePath) {
|
// Accepts intermediate patches (50+ MB) and full installs (1.5+ GB)
|
||||||
|
function validatePWRFile(filePath, expectedSize = null) {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -827,27 +930,20 @@ function validatePWRFile(filePath) {
|
|||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
const sizeInMB = stats.size / 1024 / 1024;
|
const sizeInMB = stats.size / 1024 / 1024;
|
||||||
|
|
||||||
|
// PWR files should be at least 1 MB
|
||||||
if (stats.size < 1024 * 1024) {
|
if (stats.size < 1024 * 1024) {
|
||||||
|
console.log(`[PWR Validation] File too small: ${sizeInMB.toFixed(2)} MB`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if file is under 1.5 GB (incomplete download)
|
// Validate against expected size if known (reject if < 99% of expected)
|
||||||
if (sizeInMB < 1500) {
|
if (expectedSize && stats.size < expectedSize * 0.99) {
|
||||||
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
|
const expectedMB = expectedSize / 1024 / 1024;
|
||||||
|
console.log(`[PWR Validation] File truncated: ${sizeInMB.toFixed(2)} MB, expected ${expectedMB.toFixed(2)} MB`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic file header validation (PWR files should have specific headers)
|
console.log(`[PWR Validation] File size: ${sizeInMB.toFixed(2)} MB - OK`);
|
||||||
const buffer = fs.readFileSync(filePath, { start: 0, end: 20 });
|
|
||||||
if (buffer.length < 10) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for common PWR magic bytes or patterns
|
|
||||||
// This is a basic check - could be enhanced with actual PWR format specification
|
|
||||||
const header = buffer.toString('hex', 0, 10);
|
|
||||||
console.log(`[PWR Validation] File header: ${header}`);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[PWR Validation] Error:`, error.message);
|
console.error(`[PWR Validation] Error:`, error.message);
|
||||||
|
|||||||
@@ -2,40 +2,248 @@ const axios = require('axios');
|
|||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { getOS, getArch } = require('../utils/platformUtils');
|
const { getOS, getArch } = require('../utils/platformUtils');
|
||||||
const { smartRequest } = require('../utils/proxyClient');
|
|
||||||
|
|
||||||
const BASE_PATCH_URL = 'https://game-patches.hytale.com/patches';
|
// Patches CDN via auth server redirect gateway (allows instant CDN switching)
|
||||||
const MANIFEST_API = 'https://files.hytalef2p.com/api/patch_manifest';
|
const AUTH_DOMAIN = process.env.HYTALE_AUTH_DOMAIN || 'auth.sanasol.ws';
|
||||||
|
const MIRROR_BASE_URL = `https://${AUTH_DOMAIN}/patches`;
|
||||||
|
const MIRROR_MANIFEST_URL = `${MIRROR_BASE_URL}/manifest.json`;
|
||||||
|
|
||||||
|
// Fallback: latest known build number if manifest is unreachable
|
||||||
|
const FALLBACK_LATEST_BUILD = 11;
|
||||||
|
|
||||||
|
let manifestCache = null;
|
||||||
|
let manifestCacheTime = 0;
|
||||||
|
const MANIFEST_CACHE_DURATION = 60000; // 1 minute
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the mirror manifest from MEGA S4
|
||||||
|
*/
|
||||||
|
async function fetchMirrorManifest() {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (manifestCache && (now - manifestCacheTime) < MANIFEST_CACHE_DURATION) {
|
||||||
|
console.log('[Mirror] Using cached manifest');
|
||||||
|
return manifestCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[Mirror] Fetching manifest from:', MIRROR_MANIFEST_URL);
|
||||||
|
const response = await axios.get(MIRROR_MANIFEST_URL, {
|
||||||
|
timeout: 15000,
|
||||||
|
headers: { 'User-Agent': 'Hytale-F2P-Launcher' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data && response.data.files) {
|
||||||
|
manifestCache = response.data;
|
||||||
|
manifestCacheTime = now;
|
||||||
|
console.log('[Mirror] Manifest fetched successfully');
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
throw new Error('Invalid manifest structure');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Mirror] Error fetching manifest:', error.message);
|
||||||
|
if (manifestCache) {
|
||||||
|
console.log('[Mirror] Using expired cache');
|
||||||
|
return manifestCache;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse manifest to get available patches for current platform
|
||||||
|
* Returns array of { from, to, key, size }
|
||||||
|
*/
|
||||||
|
function getPlatformPatches(manifest, branch = 'release') {
|
||||||
|
const os = getOS();
|
||||||
|
const arch = getArch();
|
||||||
|
const prefix = `${os}/${arch}/${branch}/`;
|
||||||
|
const patches = [];
|
||||||
|
|
||||||
|
for (const [key, info] of Object.entries(manifest.files)) {
|
||||||
|
if (key.startsWith(prefix) && key.endsWith('.pwr')) {
|
||||||
|
const filename = key.slice(prefix.length, -4); // e.g., "0_to_11"
|
||||||
|
const match = filename.match(/^(\d+)_to_(\d+)$/);
|
||||||
|
if (match) {
|
||||||
|
patches.push({
|
||||||
|
from: parseInt(match[1]),
|
||||||
|
to: parseInt(match[2]),
|
||||||
|
key,
|
||||||
|
size: info.size
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return patches;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find optimal patch path using BFS with download size minimization
|
||||||
|
* Returns array of { from, to, url, size, key } steps, or null if no path found
|
||||||
|
*/
|
||||||
|
function findOptimalPatchPath(currentBuild, targetBuild, patches) {
|
||||||
|
if (currentBuild >= targetBuild) return [];
|
||||||
|
|
||||||
|
const edges = {};
|
||||||
|
for (const patch of patches) {
|
||||||
|
if (!edges[patch.from]) edges[patch.from] = [];
|
||||||
|
edges[patch.from].push(patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue = [{ build: currentBuild, path: [], totalSize: 0 }];
|
||||||
|
let bestPath = null;
|
||||||
|
let bestSize = Infinity;
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const { build, path, totalSize } = queue.shift();
|
||||||
|
|
||||||
|
if (build === targetBuild) {
|
||||||
|
if (totalSize < bestSize) {
|
||||||
|
bestPath = path;
|
||||||
|
bestSize = totalSize;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalSize >= bestSize) continue;
|
||||||
|
|
||||||
|
const nextEdges = edges[build] || [];
|
||||||
|
for (const edge of nextEdges) {
|
||||||
|
if (edge.to <= build || edge.to > targetBuild) continue;
|
||||||
|
if (path.some(p => p.to === edge.to)) continue;
|
||||||
|
|
||||||
|
queue.push({
|
||||||
|
build: edge.to,
|
||||||
|
path: [...path, {
|
||||||
|
from: edge.from,
|
||||||
|
to: edge.to,
|
||||||
|
url: `${MIRROR_BASE_URL}/${edge.key}`,
|
||||||
|
size: edge.size,
|
||||||
|
key: edge.key
|
||||||
|
}],
|
||||||
|
totalSize: totalSize + edge.size
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the optimal update plan from currentBuild to targetBuild
|
||||||
|
* Returns { steps: [{from, to, url, size}], totalSize, isFullInstall }
|
||||||
|
*/
|
||||||
|
async function getUpdatePlan(currentBuild, targetBuild, branch = 'release') {
|
||||||
|
const manifest = await fetchMirrorManifest();
|
||||||
|
const patches = getPlatformPatches(manifest, branch);
|
||||||
|
|
||||||
|
// Try optimal path
|
||||||
|
const steps = findOptimalPatchPath(currentBuild, targetBuild, patches);
|
||||||
|
|
||||||
|
if (steps && steps.length > 0) {
|
||||||
|
const totalSize = steps.reduce((sum, s) => sum + s.size, 0);
|
||||||
|
console.log(`[Mirror] Update plan: ${steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')} (${(totalSize / 1024 / 1024).toFixed(0)} MB)`);
|
||||||
|
return { steps, totalSize, isFullInstall: steps.length === 1 && steps[0].from === 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: full install 0 -> target
|
||||||
|
const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
|
||||||
|
if (fullPatch) {
|
||||||
|
const step = {
|
||||||
|
from: 0,
|
||||||
|
to: targetBuild,
|
||||||
|
url: `${MIRROR_BASE_URL}/${fullPatch.key}`,
|
||||||
|
size: fullPatch.size,
|
||||||
|
key: fullPatch.key
|
||||||
|
};
|
||||||
|
console.log(`[Mirror] Full install: 0\u2192${targetBuild} (${(fullPatch.size / 1024 / 1024).toFixed(0)} MB)`);
|
||||||
|
return { steps: [step], totalSize: fullPatch.size, isFullInstall: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`No patch path found from build ${currentBuild} to ${targetBuild} for ${getOS()}/${getArch()}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function getLatestClientVersion(branch = 'release') {
|
async function getLatestClientVersion(branch = 'release') {
|
||||||
try {
|
try {
|
||||||
console.log(`Fetching latest client version from API (branch: ${branch})...`);
|
console.log(`[Mirror] Fetching latest client version (branch: ${branch})...`);
|
||||||
const response = await smartRequest(`https://files.hytalef2p.com/api/version_client?branch=${branch}`, {
|
const manifest = await fetchMirrorManifest();
|
||||||
timeout: 40000,
|
const patches = getPlatformPatches(manifest, branch);
|
||||||
headers: {
|
|
||||||
'User-Agent': 'Hytale-F2P-Launcher'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data && response.data.client_version) {
|
if (patches.length === 0) {
|
||||||
const version = response.data.client_version;
|
console.log(`[Mirror] No patches for branch '${branch}', using fallback`);
|
||||||
console.log(`Latest client version for ${branch}: ${version}`);
|
return `v${FALLBACK_LATEST_BUILD}`;
|
||||||
return version;
|
}
|
||||||
|
|
||||||
|
const latestBuild = Math.max(...patches.map(p => p.to));
|
||||||
|
console.log(`[Mirror] Latest client version: v${latestBuild}`);
|
||||||
|
return `v${latestBuild}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Mirror] Error:', error.message);
|
||||||
|
return `v${FALLBACK_LATEST_BUILD}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get PWR download URL for fresh install (0 -> target)
|
||||||
|
* Backward-compatible with old getPWRUrlFromNewAPI signature
|
||||||
|
* Checks mirror first, then constructs URL for the branch
|
||||||
|
*/
|
||||||
|
async function getPWRUrl(branch = 'release', version = 'v11') {
|
||||||
|
const targetBuild = extractVersionNumber(version);
|
||||||
|
const os = getOS();
|
||||||
|
const arch = getArch();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const manifest = await fetchMirrorManifest();
|
||||||
|
const patches = getPlatformPatches(manifest, branch);
|
||||||
|
const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
|
||||||
|
|
||||||
|
if (fullPatch) {
|
||||||
|
const url = `${MIRROR_BASE_URL}/${fullPatch.key}`;
|
||||||
|
console.log(`[Mirror] PWR URL: ${url}`);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patches.length > 0) {
|
||||||
|
// Branch exists in mirror but no full patch for this target - construct URL
|
||||||
|
console.log(`[Mirror] No 0->${targetBuild} patch found, constructing URL`);
|
||||||
} else {
|
} else {
|
||||||
console.log('Warning: Invalid API response, falling back to latest known version (7.pwr)');
|
console.log(`[Mirror] Branch '${branch}' not in mirror, constructing URL`);
|
||||||
return '7.pwr';
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching client version:', error.message);
|
console.error('[Mirror] Error getting PWR URL:', error.message);
|
||||||
console.log('Warning: API unavailable, falling back to latest known version (7.pwr)');
|
|
||||||
return '7.pwr';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construct mirror URL (will work if patch was uploaded but manifest is stale)
|
||||||
|
return `${MIRROR_BASE_URL}/${os}/${arch}/${branch}/0_to_${targetBuild}.pwr`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward-compatible alias
|
||||||
|
const getPWRUrlFromNewAPI = getPWRUrl;
|
||||||
|
|
||||||
|
// Utility function to extract version number
|
||||||
|
// Supports: "7.pwr", "v8", "v8-windows-amd64.pwr", "5_to_10", etc.
|
||||||
|
function extractVersionNumber(version) {
|
||||||
|
if (!version) return 0;
|
||||||
|
|
||||||
|
// New format: "v8" or "v8-xxx.pwr"
|
||||||
|
const vMatch = version.match(/v(\d+)/);
|
||||||
|
if (vMatch) return parseInt(vMatch[1]);
|
||||||
|
|
||||||
|
// Old format: "7.pwr"
|
||||||
|
const pwrMatch = version.match(/(\d+)\.pwr/);
|
||||||
|
if (pwrMatch) return parseInt(pwrMatch[1]);
|
||||||
|
|
||||||
|
// Fallback
|
||||||
|
const num = parseInt(version);
|
||||||
|
return isNaN(num) ? 0 : num;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildArchiveUrl(buildNumber, branch = 'release') {
|
function buildArchiveUrl(buildNumber, branch = 'release') {
|
||||||
const os = getOS();
|
const os = getOS();
|
||||||
const arch = getArch();
|
const arch = getArch();
|
||||||
return `${BASE_PATCH_URL}/${os}/${arch}/${branch}/0/${buildNumber}.pwr`;
|
return `${MIRROR_BASE_URL}/${os}/${arch}/${branch}/0_to_${buildNumber}.pwr`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkArchiveExists(buildNumber, branch = 'release') {
|
async function checkArchiveExists(buildNumber, branch = 'release') {
|
||||||
@@ -43,91 +251,56 @@ async function checkArchiveExists(buildNumber, branch = 'release') {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.head(url, { timeout: 10000 });
|
const response = await axios.head(url, { timeout: 10000 });
|
||||||
return response.status === 200;
|
return response.status === 200;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function discoverAvailableVersions(latestKnown, branch = 'release', maxProbe = 50) {
|
async function discoverAvailableVersions(latestKnown, branch = 'release') {
|
||||||
const available = [];
|
|
||||||
const latest = parseInt(latestKnown.replace('.pwr', ''));
|
|
||||||
|
|
||||||
for (let i = latest; i >= Math.max(1, latest - maxProbe); i--) {
|
|
||||||
const exists = await checkArchiveExists(i, branch);
|
|
||||||
if (exists) {
|
|
||||||
available.push(`${i}.pwr`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return available;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchPatchManifest(branch = 'release') {
|
|
||||||
try {
|
try {
|
||||||
const os = getOS();
|
const manifest = await fetchMirrorManifest();
|
||||||
const arch = getArch();
|
const patches = getPlatformPatches(manifest, branch);
|
||||||
const response = await smartRequest(`${MANIFEST_API}?branch=${branch}&os=${os}&arch=${arch}`, {
|
const versions = [...new Set(patches.map(p => p.to))].sort((a, b) => b - a);
|
||||||
timeout: 10000
|
return versions.map(v => `${v}.pwr`);
|
||||||
});
|
} catch {
|
||||||
return response.data.patches || {};
|
return [];
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch patch manifest:', error.message);
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractVersionDetails(targetVersion, branch = 'release') {
|
async function extractVersionDetails(targetVersion, branch = 'release') {
|
||||||
const buildNumber = parseInt(targetVersion.replace('.pwr', ''));
|
const buildNumber = extractVersionNumber(targetVersion);
|
||||||
const previousBuild = buildNumber - 1;
|
const fullUrl = buildArchiveUrl(buildNumber, branch);
|
||||||
|
|
||||||
const manifest = await fetchPatchManifest(branch);
|
|
||||||
const patchInfo = manifest[buildNumber];
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: targetVersion,
|
version: targetVersion,
|
||||||
buildNumber: buildNumber,
|
buildNumber,
|
||||||
buildName: `HYTALE-Build-${buildNumber}`,
|
buildName: `HYTALE-Build-${buildNumber}`,
|
||||||
fullUrl: patchInfo?.original_url || buildArchiveUrl(buildNumber, branch),
|
fullUrl,
|
||||||
differentialUrl: patchInfo?.patch_url || null,
|
differentialUrl: null,
|
||||||
checksum: patchInfo?.patch_hash || null,
|
checksum: null,
|
||||||
sourceVersion: patchInfo?.from ? `${patchInfo.from}.pwr` : (previousBuild > 0 ? `${previousBuild}.pwr` : null),
|
sourceVersion: null,
|
||||||
isDifferential: !!patchInfo?.proper_patch,
|
isDifferential: false,
|
||||||
releaseNotes: patchInfo?.patch_note || null
|
releaseNotes: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function canUseDifferentialUpdate(currentVersion, targetDetails) {
|
function canUseDifferentialUpdate() {
|
||||||
if (!targetDetails) return false;
|
// Differential updates are now handled via getUpdatePlan()
|
||||||
if (!targetDetails.differentialUrl) return false;
|
return false;
|
||||||
if (!targetDetails.isDifferential) return false;
|
|
||||||
|
|
||||||
if (!currentVersion) return false;
|
|
||||||
|
|
||||||
const currentBuild = parseInt(currentVersion.replace('.pwr', ''));
|
|
||||||
const expectedSource = parseInt(targetDetails.sourceVersion?.replace('.pwr', '') || '0');
|
|
||||||
|
|
||||||
return currentBuild === expectedSource;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function needsIntermediatePatches(currentVersion, targetVersion) {
|
function needsIntermediatePatches(currentVersion, targetVersion) {
|
||||||
if (!currentVersion) return [];
|
if (!currentVersion) return [];
|
||||||
|
const current = extractVersionNumber(currentVersion);
|
||||||
const current = parseInt(currentVersion.replace('.pwr', ''));
|
const target = extractVersionNumber(targetVersion);
|
||||||
const target = parseInt(targetVersion.replace('.pwr', ''));
|
if (current >= target) return [];
|
||||||
|
return [targetVersion];
|
||||||
const intermediates = [];
|
|
||||||
for (let i = current + 1; i <= target; i++) {
|
|
||||||
intermediates.push(`${i}.pwr`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return intermediates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function computeFileChecksum(filePath) {
|
async function computeFileChecksum(filePath) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const hash = crypto.createHash('sha256');
|
const hash = crypto.createHash('sha256');
|
||||||
const stream = fs.createReadStream(filePath);
|
const stream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
stream.on('data', data => hash.update(data));
|
stream.on('data', data => hash.update(data));
|
||||||
stream.on('end', () => resolve(hash.digest('hex')));
|
stream.on('end', () => resolve(hash.digest('hex')));
|
||||||
stream.on('error', reject);
|
stream.on('error', reject);
|
||||||
@@ -136,7 +309,6 @@ async function computeFileChecksum(filePath) {
|
|||||||
|
|
||||||
async function validateChecksum(filePath, expectedChecksum) {
|
async function validateChecksum(filePath, expectedChecksum) {
|
||||||
if (!expectedChecksum) return true;
|
if (!expectedChecksum) return true;
|
||||||
|
|
||||||
const actualChecksum = await computeFileChecksum(filePath);
|
const actualChecksum = await computeFileChecksum(filePath);
|
||||||
return actualChecksum === expectedChecksum;
|
return actualChecksum === expectedChecksum;
|
||||||
}
|
}
|
||||||
@@ -145,7 +317,7 @@ function getInstalledClientVersion() {
|
|||||||
try {
|
try {
|
||||||
const { loadVersionClient } = require('../core/config');
|
const { loadVersionClient } = require('../core/config');
|
||||||
return loadVersionClient();
|
return loadVersionClient();
|
||||||
} catch (err) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,5 +332,13 @@ module.exports = {
|
|||||||
needsIntermediatePatches,
|
needsIntermediatePatches,
|
||||||
computeFileChecksum,
|
computeFileChecksum,
|
||||||
validateChecksum,
|
validateChecksum,
|
||||||
getInstalledClientVersion
|
getInstalledClientVersion,
|
||||||
|
fetchMirrorManifest,
|
||||||
|
getPWRUrl,
|
||||||
|
getPWRUrlFromNewAPI,
|
||||||
|
getUpdatePlan,
|
||||||
|
extractVersionNumber,
|
||||||
|
getPlatformPatches,
|
||||||
|
findOptimalPatchPath,
|
||||||
|
MIRROR_BASE_URL
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
provider: github
|
provider: generic
|
||||||
owner: amiayweb # Change to your own GitHub username
|
url: https://git.sanhost.net/sanasol/hytale-f2p/releases/download/latest
|
||||||
repo: Hytale-F2P
|
|
||||||
|
|||||||
10
main.js
10
main.js
@@ -89,7 +89,7 @@ function setDiscordActivity() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Discord',
|
label: 'Discord',
|
||||||
url: 'https://discord.gg/hf2pdc'
|
url: 'https://discord.gg/Fhbb9Yk5WW'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -627,7 +627,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
|||||||
console.log('[Main] Processing Butler error with retry context');
|
console.log('[Main] Processing Butler error with retry context');
|
||||||
errorData.retryData = {
|
errorData.retryData = {
|
||||||
branch: error.branch || 'release',
|
branch: error.branch || 'release',
|
||||||
fileName: error.fileName || '7.pwr',
|
fileName: error.fileName || 'v8',
|
||||||
cacheDir: error.cacheDir
|
cacheDir: error.cacheDir
|
||||||
};
|
};
|
||||||
errorData.canRetry = error.canRetry !== false;
|
errorData.canRetry = error.canRetry !== false;
|
||||||
@@ -647,7 +647,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
|||||||
console.log('[Main] Processing generic error, creating default retry data');
|
console.log('[Main] Processing generic error, creating default retry data');
|
||||||
errorData.retryData = {
|
errorData.retryData = {
|
||||||
branch: 'release',
|
branch: 'release',
|
||||||
fileName: '7.pwr'
|
fileName: 'v8'
|
||||||
};
|
};
|
||||||
// For generic errors, assume it's retryable unless specified
|
// For generic errors, assume it's retryable unless specified
|
||||||
errorData.canRetry = error.canRetry !== false;
|
errorData.canRetry = error.canRetry !== false;
|
||||||
@@ -887,7 +887,7 @@ ipcMain.handle('retry-download', async (event, retryData) => {
|
|||||||
console.log('[IPC] Invalid retry data, using PWR defaults');
|
console.log('[IPC] Invalid retry data, using PWR defaults');
|
||||||
retryData = {
|
retryData = {
|
||||||
branch: 'release',
|
branch: 'release',
|
||||||
fileName: '7.pwr'
|
fileName: 'v8'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,7 +921,7 @@ ipcMain.handle('retry-download', async (event, retryData) => {
|
|||||||
} :
|
} :
|
||||||
{
|
{
|
||||||
branch: retryData?.branch || 'release',
|
branch: retryData?.branch || 'release',
|
||||||
fileName: retryData?.fileName || '7.pwr',
|
fileName: retryData?.fileName || 'v8',
|
||||||
cacheDir: retryData?.cacheDir
|
cacheDir: retryData?.cacheDir
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.2.1",
|
"version": "2.3.2",
|
||||||
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
||||||
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
@@ -118,9 +118,8 @@
|
|||||||
"createStartMenuShortcut": true
|
"createStartMenuShortcut": true
|
||||||
},
|
},
|
||||||
"publish": {
|
"publish": {
|
||||||
"provider": "github",
|
"provider": "generic",
|
||||||
"owner": "amiayweb",
|
"url": "https://git.sanhost.net/sanasol/hytale-f2p/releases/download/latest"
|
||||||
"repo": "Hytale-F2P"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user