Compare commits

..

3 Commits

Author SHA1 Message Date
sanasol
fdd8e59ec4 v2.3.3: fix singleplayer crash when install path has spaces
JAVA_TOOL_OPTIONS -javaagent path was not quoted, causing JVM to
truncate at first space. Affects all users with spaces in install
path (e.g. "Hytale F2P Launcher").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 15:54:17 +01:00
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
13 changed files with 62 additions and 239 deletions

View File

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

View File

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

3
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

@@ -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**

View File

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

View File

@@ -439,7 +439,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;

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 osName = getOS();
const arch = getArch(); const arch = getArch();
@@ -90,17 +90,47 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
console.log(`[DownloadPWR] Fallback URL: ${url}`); 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`); 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) {
const stats = fs.statSync(dest); const stats = fs.statSync(dest);
if (stats.size > 1024 * 1024) { if (stats.size > 1024 * 1024) {
console.log(`[PWR] Using cached file: ${dest} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`); // Validate against expected size - reject if file is truncated (< 99% of expected)
return dest; 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}`); console.log(`[DownloadPWR] Downloading from: ${url}`);
@@ -129,7 +159,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)) { 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');
@@ -179,8 +209,8 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
// Enhanced PWR file validation // Enhanced PWR file validation
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');
@@ -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); 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) { if (!pwrFile) {
throw new Error(`Failed to download patch ${stepName}`); throw new Error(`Failed to download patch ${stepName}`);
@@ -891,7 +921,7 @@ function validateGameDirectory(gameDir, stagingDir) {
// Enhanced PWR file validation // Enhanced PWR file validation
// Accepts intermediate patches (50+ MB) and full installs (1.5+ GB) // 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;
@@ -906,6 +936,13 @@ function validatePWRFile(filePath) {
return false; 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`); console.log(`[PWR Validation] File size: ${sizeInMB.toFixed(2)} MB - OK`);
return true; return true;
} catch (error) { } 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', label: 'Discord',
url: 'https://discord.gg/hf2pdc' url: 'https://discord.gg/Fhbb9Yk5WW'
} }
] ]
}); });

View File

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