mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-27 18:01:47 -03:00
Compare commits
5 Commits
v2.3.1
...
8435fc698c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8435fc698c | ||
|
|
6c369edb0f | ||
|
|
fdd8e59ec4 | ||
|
|
e7a033932f | ||
|
|
11c6d40dfe |
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
|
||||
|
||||
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.
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/support_request.yml
vendored
2
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -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
3
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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**
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -439,7 +439,7 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
// This enables runtime auth patching without modifying the server JAR
|
||||
const agentJar = path.join(gameLatest, 'Server', 'dualauth-agent.jar');
|
||||
if (fs.existsSync(agentJar)) {
|
||||
const agentFlag = `-javaagent:${agentJar}`;
|
||||
const agentFlag = `-javaagent:"${agentJar}"`;
|
||||
env.JAVA_TOOL_OPTIONS = env.JAVA_TOOL_OPTIONS
|
||||
? `${env.JAVA_TOOL_OPTIONS} ${agentFlag}`
|
||||
: agentFlag;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -85,22 +85,53 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
|
||||
console.log(`[DownloadPWR] Mirror URL: ${url}`);
|
||||
} catch (error) {
|
||||
console.error(`[DownloadPWR] Failed to get mirror URL: ${error.message}`);
|
||||
const { MIRROR_BASE_URL } = require('../services/versionManager');
|
||||
url = `${MIRROR_BASE_URL}/${osName}/${arch}/${branch}/0_to_${extractVersionNumber(fileName)}.pwr`;
|
||||
const { getPatchesBaseUrl } = require('../services/versionManager');
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
url = `${baseUrl}/${osName}/${arch}/${branch}/0_to_${extractVersionNumber(fileName)}.pwr`;
|
||||
console.log(`[DownloadPWR] Fallback URL: ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Look up expected file size from manifest if not provided
|
||||
if (!expectedSize) {
|
||||
try {
|
||||
const { fetchMirrorManifest } = require('../services/versionManager');
|
||||
const manifest = await fetchMirrorManifest();
|
||||
// Try to match: "0_to_11" format or "v11" format
|
||||
const versionMatch = fileName.match(/^(\d+)_to_(\d+)$/);
|
||||
let manifestKey;
|
||||
if (versionMatch) {
|
||||
manifestKey = `${osName}/${arch}/${branch}/${fileName}.pwr`;
|
||||
} else {
|
||||
const buildNum = extractVersionNumber(fileName);
|
||||
manifestKey = `${osName}/${arch}/${branch}/0_to_${buildNum}.pwr`;
|
||||
}
|
||||
if (manifest.files[manifestKey]) {
|
||||
expectedSize = manifest.files[manifestKey].size;
|
||||
console.log(`[PWR] Expected size from manifest: ${(expectedSize / 1024 / 1024).toFixed(2)} MB`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`[PWR] Could not fetch expected size from manifest: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const dest = path.join(cacheDir, `${branch}_${fileName}.pwr`);
|
||||
|
||||
// Check if file exists and validate it
|
||||
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 +160,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');
|
||||
@@ -180,7 +211,7 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
|
||||
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 +471,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 +922,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 +937,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) {
|
||||
|
||||
@@ -3,20 +3,56 @@ const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const { getOS, getArch } = require('../utils/platformUtils');
|
||||
|
||||
// Patches CDN via auth server redirect gateway (allows instant CDN switching)
|
||||
// Patches base URL fetched dynamically from auth server
|
||||
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 PATCHES_CONFIG_URL = `https://${AUTH_DOMAIN}/api/patches-config`;
|
||||
|
||||
// Fallback: latest known build number if manifest is unreachable
|
||||
const FALLBACK_LATEST_BUILD = 11;
|
||||
|
||||
let patchesBaseUrl = null;
|
||||
let patchesConfigTime = 0;
|
||||
const PATCHES_CONFIG_CACHE_DURATION = 300000; // 5 minutes
|
||||
|
||||
let manifestCache = null;
|
||||
let manifestCacheTime = 0;
|
||||
const MANIFEST_CACHE_DURATION = 60000; // 1 minute
|
||||
|
||||
/**
|
||||
* Fetch the mirror manifest from MEGA S4
|
||||
* Fetch patches base URL from auth server config endpoint
|
||||
*/
|
||||
async function getPatchesBaseUrl() {
|
||||
const now = Date.now();
|
||||
if (patchesBaseUrl && (now - patchesConfigTime) < PATCHES_CONFIG_CACHE_DURATION) {
|
||||
return patchesBaseUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[Mirror] Fetching patches config from:', PATCHES_CONFIG_URL);
|
||||
const response = await axios.get(PATCHES_CONFIG_URL, {
|
||||
timeout: 10000,
|
||||
headers: { 'User-Agent': 'Hytale-F2P-Launcher' }
|
||||
});
|
||||
|
||||
if (response.data && response.data.patches_url) {
|
||||
patchesBaseUrl = response.data.patches_url.replace(/\/+$/, '');
|
||||
patchesConfigTime = now;
|
||||
console.log('[Mirror] Patches base URL:', patchesBaseUrl);
|
||||
return patchesBaseUrl;
|
||||
}
|
||||
throw new Error('Invalid patches config');
|
||||
} catch (error) {
|
||||
console.error('[Mirror] Error fetching patches config:', error.message);
|
||||
if (patchesBaseUrl) {
|
||||
console.log('[Mirror] Using cached patches URL:', patchesBaseUrl);
|
||||
return patchesBaseUrl;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the mirror manifest
|
||||
*/
|
||||
async function fetchMirrorManifest() {
|
||||
const now = Date.now();
|
||||
@@ -26,9 +62,12 @@ async function fetchMirrorManifest() {
|
||||
return manifestCache;
|
||||
}
|
||||
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
const manifestUrl = `${baseUrl}/manifest.json`;
|
||||
|
||||
try {
|
||||
console.log('[Mirror] Fetching manifest from:', MIRROR_MANIFEST_URL);
|
||||
const response = await axios.get(MIRROR_MANIFEST_URL, {
|
||||
console.log('[Mirror] Fetching manifest from:', manifestUrl);
|
||||
const response = await axios.get(manifestUrl, {
|
||||
timeout: 15000,
|
||||
headers: { 'User-Agent': 'Hytale-F2P-Launcher' }
|
||||
});
|
||||
@@ -82,9 +121,10 @@ function getPlatformPatches(manifest, branch = 'release') {
|
||||
* 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) {
|
||||
async function findOptimalPatchPath(currentBuild, targetBuild, patches) {
|
||||
if (currentBuild >= targetBuild) return [];
|
||||
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
const edges = {};
|
||||
for (const patch of patches) {
|
||||
if (!edges[patch.from]) edges[patch.from] = [];
|
||||
@@ -118,7 +158,7 @@ function findOptimalPatchPath(currentBuild, targetBuild, patches) {
|
||||
path: [...path, {
|
||||
from: edge.from,
|
||||
to: edge.to,
|
||||
url: `${MIRROR_BASE_URL}/${edge.key}`,
|
||||
url: `${baseUrl}/${edge.key}`,
|
||||
size: edge.size,
|
||||
key: edge.key
|
||||
}],
|
||||
@@ -139,7 +179,7 @@ async function getUpdatePlan(currentBuild, targetBuild, branch = 'release') {
|
||||
const patches = getPlatformPatches(manifest, branch);
|
||||
|
||||
// Try optimal path
|
||||
const steps = findOptimalPatchPath(currentBuild, targetBuild, patches);
|
||||
const steps = await findOptimalPatchPath(currentBuild, targetBuild, patches);
|
||||
|
||||
if (steps && steps.length > 0) {
|
||||
const totalSize = steps.reduce((sum, s) => sum + s.size, 0);
|
||||
@@ -150,10 +190,11 @@ async function getUpdatePlan(currentBuild, targetBuild, branch = 'release') {
|
||||
// Fallback: full install 0 -> target
|
||||
const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
|
||||
if (fullPatch) {
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
const step = {
|
||||
from: 0,
|
||||
to: targetBuild,
|
||||
url: `${MIRROR_BASE_URL}/${fullPatch.key}`,
|
||||
url: `${baseUrl}/${fullPatch.key}`,
|
||||
size: fullPatch.size,
|
||||
key: fullPatch.key
|
||||
};
|
||||
@@ -200,7 +241,8 @@ async function getPWRUrl(branch = 'release', version = 'v11') {
|
||||
const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
|
||||
|
||||
if (fullPatch) {
|
||||
const url = `${MIRROR_BASE_URL}/${fullPatch.key}`;
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
const url = `${baseUrl}/${fullPatch.key}`;
|
||||
console.log(`[Mirror] PWR URL: ${url}`);
|
||||
return url;
|
||||
}
|
||||
@@ -216,7 +258,8 @@ async function getPWRUrl(branch = 'release', version = 'v11') {
|
||||
}
|
||||
|
||||
// Construct mirror URL (will work if patch was uploaded but manifest is stale)
|
||||
return `${MIRROR_BASE_URL}/${os}/${arch}/${branch}/0_to_${targetBuild}.pwr`;
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
return `${baseUrl}/${os}/${arch}/${branch}/0_to_${targetBuild}.pwr`;
|
||||
}
|
||||
|
||||
// Backward-compatible alias
|
||||
@@ -240,14 +283,15 @@ function extractVersionNumber(version) {
|
||||
return isNaN(num) ? 0 : num;
|
||||
}
|
||||
|
||||
function buildArchiveUrl(buildNumber, branch = 'release') {
|
||||
async function buildArchiveUrl(buildNumber, branch = 'release') {
|
||||
const baseUrl = await getPatchesBaseUrl();
|
||||
const os = getOS();
|
||||
const arch = getArch();
|
||||
return `${MIRROR_BASE_URL}/${os}/${arch}/${branch}/0_to_${buildNumber}.pwr`;
|
||||
return `${baseUrl}/${os}/${arch}/${branch}/0_to_${buildNumber}.pwr`;
|
||||
}
|
||||
|
||||
async function checkArchiveExists(buildNumber, branch = 'release') {
|
||||
const url = buildArchiveUrl(buildNumber, branch);
|
||||
const url = await buildArchiveUrl(buildNumber, branch);
|
||||
try {
|
||||
const response = await axios.head(url, { timeout: 10000 });
|
||||
return response.status === 200;
|
||||
@@ -269,7 +313,7 @@ async function discoverAvailableVersions(latestKnown, branch = 'release') {
|
||||
|
||||
async function extractVersionDetails(targetVersion, branch = 'release') {
|
||||
const buildNumber = extractVersionNumber(targetVersion);
|
||||
const fullUrl = buildArchiveUrl(buildNumber, branch);
|
||||
const fullUrl = await buildArchiveUrl(buildNumber, branch);
|
||||
|
||||
return {
|
||||
version: targetVersion,
|
||||
@@ -340,5 +384,5 @@ module.exports = {
|
||||
extractVersionNumber,
|
||||
getPlatformPatches,
|
||||
findOptimalPatchPath,
|
||||
MIRROR_BASE_URL
|
||||
getPatchesBaseUrl
|
||||
};
|
||||
|
||||
@@ -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
|
||||
10
main.js
10
main.js
@@ -84,12 +84,12 @@ function setDiscordActivity() {
|
||||
largeImageText: 'Hytale F2P Launcher',
|
||||
buttons: [
|
||||
{
|
||||
label: 'GitHub',
|
||||
url: 'https://github.com/amiayweb/Hytale-F2P'
|
||||
label: 'Download',
|
||||
url: 'https://git.sanhost.net/sanasol/hytale-f2p/releases'
|
||||
},
|
||||
{
|
||||
label: 'Discord',
|
||||
url: 'https://discord.gg/hf2pdc'
|
||||
url: 'https://discord.gg/Fhbb9Yk5WW'
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -964,8 +964,8 @@ ipcMain.handle('open-external', async (event, url) => {
|
||||
|
||||
ipcMain.handle('open-download-page', async () => {
|
||||
try {
|
||||
// Open GitHub releases page for manual download
|
||||
await shell.openExternal('https://github.com/amiayweb/Hytale-F2P/releases/latest');
|
||||
// Open Forgejo releases page for manual download
|
||||
await shell.openExternal('https://git.sanhost.net/sanasol/hytale-f2p/releases/latest');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to open download page:', error);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "hytale-f2p-launcher",
|
||||
"version": "2.3.1",
|
||||
"version": "2.3.4",
|
||||
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
||||
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
||||
"homepage": "https://git.sanhost.net/sanasol/hytale-f2p",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
|
||||
Reference in New Issue
Block a user