Compare commits

...

2 Commits

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 15:14:20 +01:00
sanasol
11c6d40dfe chore: remove private docs from repo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 14:38:34 +01:00
12 changed files with 61 additions and 238 deletions

View File

@@ -36,7 +36,7 @@ This Code of Conduct applies within all community spaces, and also applies when
## 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.

View File

@@ -22,7 +22,7 @@ body:
value: |
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.
**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
id: question

3
.gitignore vendored
View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@
### ⚠️ **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>
👍 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">
**Questions? Ads? Collaboration? Endorsement? Other business-related?**
Message the founders at https://discord.gg/hf2pdc
Message the founders at https://discord.gg/Fhbb9Yk5WW
</div>

View File

@@ -2,7 +2,7 @@
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**

View File

@@ -1,6 +1,6 @@
# 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:
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:
- Your operating system and version
- Launcher version

View File

@@ -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) {
async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback, cacheDir = CACHE_DIR, manualRetry = false, directUrl = null, expectedSize = null) {
const osName = getOS();
const arch = getArch();
@@ -90,17 +90,47 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
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
if (fs.existsSync(dest) && !manualRetry) {
const stats = fs.statSync(dest);
if (stats.size > 1024 * 1024) {
console.log(`[PWR] Using cached file: ${dest} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
return dest;
// 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(`[PWR] Cached file too small (${stats.size} bytes), re-downloading`);
}
console.log(`[DownloadPWR] Downloading from: ${url}`);
@@ -129,7 +159,7 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
const retryStats = fs.statSync(dest);
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}`);
fs.unlinkSync(dest);
throw new Error('Downloaded PWR file is corrupted or invalid after automatic retry. Please retry manually');
@@ -179,8 +209,8 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
// Enhanced PWR file validation
const stats = fs.statSync(dest);
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}`);
fs.unlinkSync(dest);
throw new Error('Downloaded PWR file is corrupted or invalid. Please retry');
@@ -440,7 +470,7 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
progressCallback(`Downloading patch ${i + 1}/${updatePlan.steps.length} (${stepName})...`, progress, null, null, null);
}
const pwrFile = await downloadPWR(branch, stepName, progressCallback, cacheDir, false, step.url);
const pwrFile = await downloadPWR(branch, stepName, progressCallback, cacheDir, false, step.url, step.size);
if (!pwrFile) {
throw new Error(`Failed to download patch ${stepName}`);
@@ -891,7 +921,7 @@ function validateGameDirectory(gameDir, stagingDir) {
// Enhanced PWR file validation
// Accepts intermediate patches (50+ MB) and full installs (1.5+ GB)
function validatePWRFile(filePath) {
function validatePWRFile(filePath, expectedSize = null) {
try {
if (!fs.existsSync(filePath)) {
return false;
@@ -906,6 +936,13 @@ function validatePWRFile(filePath) {
return false;
}
// Validate against expected size if known (reject if < 99% of expected)
if (expectedSize && stats.size < expectedSize * 0.99) {
const expectedMB = expectedSize / 1024 / 1024;
console.log(`[PWR Validation] File truncated: ${sizeInMB.toFixed(2)} MB, expected ${expectedMB.toFixed(2)} MB`);
return false;
}
console.log(`[PWR Validation] File size: ${sizeInMB.toFixed(2)} MB - OK`);
return true;
} catch (error) {

View File

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

View File

@@ -89,7 +89,7 @@ function setDiscordActivity() {
},
{
label: 'Discord',
url: 'https://discord.gg/hf2pdc'
url: 'https://discord.gg/Fhbb9Yk5WW'
}
]
});

View File

@@ -1,6 +1,6 @@
{
"name": "hytale-f2p-launcher",
"version": "2.3.1",
"version": "2.3.2",
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
"homepage": "https://github.com/amiayweb/Hytale-F2P",
"main": "main.js",