Compare commits

...

3 Commits

Author SHA1 Message Date
sanasol
8435fc698c fix: replace GitHub URLs with Forgejo after DMCA takedown
GitHub repo amiayweb/Hytale-F2P was DMCA'd. Updated Discord RPC link,
download page URL, and homepage to point to Forgejo instance.
Auto-update already pointed to git.sanhost.net (no change needed).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:32:37 +01:00
sanasol
6c369edb0f v2.3.4: dynamic patches URL from auth server
Launcher now fetches patches base URL from /api/patches-config endpoint
instead of using hardcoded domain. URL cached for 5 minutes, no fallback
to hardcoded domain - requires auth server connection or cached URL.
Enables instant CDN switching without launcher updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 19:27:55 +01:00
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
5 changed files with 71 additions and 26 deletions

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

@@ -85,8 +85,9 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
console.log(`[DownloadPWR] Mirror URL: ${url}`); console.log(`[DownloadPWR] Mirror URL: ${url}`);
} catch (error) { } catch (error) {
console.error(`[DownloadPWR] Failed to get mirror URL: ${error.message}`); console.error(`[DownloadPWR] Failed to get mirror URL: ${error.message}`);
const { MIRROR_BASE_URL } = require('../services/versionManager'); const { getPatchesBaseUrl } = require('../services/versionManager');
url = `${MIRROR_BASE_URL}/${osName}/${arch}/${branch}/0_to_${extractVersionNumber(fileName)}.pwr`; const baseUrl = await getPatchesBaseUrl();
url = `${baseUrl}/${osName}/${arch}/${branch}/0_to_${extractVersionNumber(fileName)}.pwr`;
console.log(`[DownloadPWR] Fallback URL: ${url}`); console.log(`[DownloadPWR] Fallback URL: ${url}`);
} }
} }

View File

@@ -3,20 +3,56 @@ const crypto = require('crypto');
const fs = require('fs'); const fs = require('fs');
const { getOS, getArch } = require('../utils/platformUtils'); 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 AUTH_DOMAIN = process.env.HYTALE_AUTH_DOMAIN || 'auth.sanasol.ws';
const MIRROR_BASE_URL = `https://${AUTH_DOMAIN}/patches`; const PATCHES_CONFIG_URL = `https://${AUTH_DOMAIN}/api/patches-config`;
const MIRROR_MANIFEST_URL = `${MIRROR_BASE_URL}/manifest.json`;
// Fallback: latest known build number if manifest is unreachable // Fallback: latest known build number if manifest is unreachable
const FALLBACK_LATEST_BUILD = 11; const FALLBACK_LATEST_BUILD = 11;
let patchesBaseUrl = null;
let patchesConfigTime = 0;
const PATCHES_CONFIG_CACHE_DURATION = 300000; // 5 minutes
let manifestCache = null; let manifestCache = null;
let manifestCacheTime = 0; let manifestCacheTime = 0;
const MANIFEST_CACHE_DURATION = 60000; // 1 minute 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() { async function fetchMirrorManifest() {
const now = Date.now(); const now = Date.now();
@@ -26,9 +62,12 @@ async function fetchMirrorManifest() {
return manifestCache; return manifestCache;
} }
const baseUrl = await getPatchesBaseUrl();
const manifestUrl = `${baseUrl}/manifest.json`;
try { try {
console.log('[Mirror] Fetching manifest from:', MIRROR_MANIFEST_URL); console.log('[Mirror] Fetching manifest from:', manifestUrl);
const response = await axios.get(MIRROR_MANIFEST_URL, { const response = await axios.get(manifestUrl, {
timeout: 15000, timeout: 15000,
headers: { 'User-Agent': 'Hytale-F2P-Launcher' } 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 * 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 * 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 []; if (currentBuild >= targetBuild) return [];
const baseUrl = await getPatchesBaseUrl();
const edges = {}; const edges = {};
for (const patch of patches) { for (const patch of patches) {
if (!edges[patch.from]) edges[patch.from] = []; if (!edges[patch.from]) edges[patch.from] = [];
@@ -118,7 +158,7 @@ function findOptimalPatchPath(currentBuild, targetBuild, patches) {
path: [...path, { path: [...path, {
from: edge.from, from: edge.from,
to: edge.to, to: edge.to,
url: `${MIRROR_BASE_URL}/${edge.key}`, url: `${baseUrl}/${edge.key}`,
size: edge.size, size: edge.size,
key: edge.key key: edge.key
}], }],
@@ -139,7 +179,7 @@ async function getUpdatePlan(currentBuild, targetBuild, branch = 'release') {
const patches = getPlatformPatches(manifest, branch); const patches = getPlatformPatches(manifest, branch);
// Try optimal path // Try optimal path
const steps = findOptimalPatchPath(currentBuild, targetBuild, patches); const steps = await findOptimalPatchPath(currentBuild, targetBuild, patches);
if (steps && steps.length > 0) { if (steps && steps.length > 0) {
const totalSize = steps.reduce((sum, s) => sum + s.size, 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 // Fallback: full install 0 -> target
const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild); const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
if (fullPatch) { if (fullPatch) {
const baseUrl = await getPatchesBaseUrl();
const step = { const step = {
from: 0, from: 0,
to: targetBuild, to: targetBuild,
url: `${MIRROR_BASE_URL}/${fullPatch.key}`, url: `${baseUrl}/${fullPatch.key}`,
size: fullPatch.size, size: fullPatch.size,
key: fullPatch.key 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); const fullPatch = patches.find(p => p.from === 0 && p.to === targetBuild);
if (fullPatch) { if (fullPatch) {
const url = `${MIRROR_BASE_URL}/${fullPatch.key}`; const baseUrl = await getPatchesBaseUrl();
const url = `${baseUrl}/${fullPatch.key}`;
console.log(`[Mirror] PWR URL: ${url}`); console.log(`[Mirror] PWR URL: ${url}`);
return 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) // 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 // Backward-compatible alias
@@ -240,14 +283,15 @@ function extractVersionNumber(version) {
return isNaN(num) ? 0 : num; return isNaN(num) ? 0 : num;
} }
function buildArchiveUrl(buildNumber, branch = 'release') { async function buildArchiveUrl(buildNumber, branch = 'release') {
const baseUrl = await getPatchesBaseUrl();
const os = getOS(); const os = getOS();
const arch = getArch(); 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') { async function checkArchiveExists(buildNumber, branch = 'release') {
const url = buildArchiveUrl(buildNumber, branch); const url = await buildArchiveUrl(buildNumber, branch);
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;
@@ -269,7 +313,7 @@ async function discoverAvailableVersions(latestKnown, branch = 'release') {
async function extractVersionDetails(targetVersion, branch = 'release') { async function extractVersionDetails(targetVersion, branch = 'release') {
const buildNumber = extractVersionNumber(targetVersion); const buildNumber = extractVersionNumber(targetVersion);
const fullUrl = buildArchiveUrl(buildNumber, branch); const fullUrl = await buildArchiveUrl(buildNumber, branch);
return { return {
version: targetVersion, version: targetVersion,
@@ -340,5 +384,5 @@ module.exports = {
extractVersionNumber, extractVersionNumber,
getPlatformPatches, getPlatformPatches,
findOptimalPatchPath, findOptimalPatchPath,
MIRROR_BASE_URL getPatchesBaseUrl
}; };

View File

@@ -84,8 +84,8 @@ function setDiscordActivity() {
largeImageText: 'Hytale F2P Launcher', largeImageText: 'Hytale F2P Launcher',
buttons: [ buttons: [
{ {
label: 'GitHub', label: 'Download',
url: 'https://github.com/amiayweb/Hytale-F2P' url: 'https://git.sanhost.net/sanasol/hytale-f2p/releases'
}, },
{ {
label: 'Discord', label: 'Discord',
@@ -964,8 +964,8 @@ ipcMain.handle('open-external', async (event, url) => {
ipcMain.handle('open-download-page', async () => { ipcMain.handle('open-download-page', async () => {
try { try {
// Open GitHub releases page for manual download // Open Forgejo releases page for manual download
await shell.openExternal('https://github.com/amiayweb/Hytale-F2P/releases/latest'); await shell.openExternal('https://git.sanhost.net/sanasol/hytale-f2p/releases/latest');
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Failed to open download page:', error); console.error('Failed to open download page:', error);

View File

@@ -1,8 +1,8 @@
{ {
"name": "hytale-f2p-launcher", "name": "hytale-f2p-launcher",
"version": "2.3.2", "version": "2.3.4",
"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://git.sanhost.net/sanasol/hytale-f2p",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",