* fix: resolve cross-platform EPERM permissions errors

modManager.js:
- Switch from hardcoded 'junction' to dynamic symlink type based on OS (fixing Linux EPERM).
- Add retry logic for directory removal to handle file locking race conditions.
- Improve broken symlink detection during profile sync.

gameManager.js:
- Implement retry loop (3 attempts) for game directory removal in updateGameFiles to prevent EBUSY/EPERM errors on Windows.

paths.js:
- Prevent fs.mkdirSync failure in getModsPath by pre-checking for broken symbolic links.

* fix: missing pacman builds

* prepare release for 2.1.1

minor fix for EPERM error permission

* prepare release 2.1.1

minor fix EPERM permission error

* prepare release 2.1.1

* Update README.md Windows Prequisites for ARM64 builds

* fix: remove broken symlink after detected

* fix: add pathexists for paths.js to check symlink

* fix: isbrokenlink should be true to remove the symlink

* add arch package .pkg.tar.zst for release

* fix: release workflow for build-arch and build-linux

* build-arch job now only build arch .pkg.tar.zst package instead of the whole generic linux.
* build-linux job now exclude .pacman package since its deprecated and should not be used.

* fix: removes pacman build as it replaced by tar.zst and adds build:arch shortcut for pkgbuild

* aur: add proper VCS (-git) PKGBUILD

created clean VCS-based PKGBUILD following arch packaging conventions.

this explicitly marked as a rolling (-git) build and derives its version dynamically from git tags and commit history via pkgver(). previous hybrid approach has been changed.

key changes:
- use -git suffix to clearly indicate rolling source builds
- set pkgver=0 and compute the actual version via pkgver()
- build only a directory layout using electron-builder (--dir)
- avoid generating AppImage, deb, rpm, or pacman installers
- align build and package steps with Arch packaging guidelines

note: this PKGBUILD is intended for development and AUR use only and is not suitable for binary redistribution or release artifacts.

* ci: add fixed-version PKGBUILD for Arch Linux releases

this PKGBUILD intended for CI and GitHub release artifacts. targets tagged releases only and uses a fixed pkgver that matches the corresponding git tag. all of the VCS logic has been removed to PKGBUILD-git to ensure reproducible builds and stable versioning suitable for binary distribution.

the build process relies on electron-builder directory output (--dir) and packages only the unpacked application into a standard Arch Linux package (.pkg.tar.zst). other distro format are excluded from this path and handled separately.

this change establishes a clear separation between:
- rolling AUR development builds (-git)
- CI-generated, versioned Arch Linux release packages

the result is predictable artifact naming, correct version alignment, and Arch-compliant packaging for downstream users.

* Update README.md

adds information for Arch build

* Update README.md

BUILD.md location was changed and now this link is poiting to nothing

* Update PKGBUILD

* Update PKGBUILD-git

* chore: fix ubuntu/debian part in README.md

* Polish language support (#195)

* Update support_request.yml

Added hardware specification

* Update bug_report.yml

Add logs textfield to bug report

* chore: add changelog in README.md

* fix screenshot input in feature_request.yml

* add hardware spec input in bug_report.yml

* fix: PKGBUILD pkgname variable fix

* userdata migration [need review from other OS]

* french translate

* Add German and Swedish translations

Added de.json and sv.json locale files for German and Swedish language support. Updated i18n.js to register 'de' and 'sv' as available languages in the launcher.

* Update README.md

* chore: add offline-mode warning to the README.md

* chore: add downloads counter in README.md

* fix: Steam Deck/Ubuntu crash - use system libzstd.so

The bundled libzstd.so is incompatible with glibc 2.41's stricter heap
validation, causing "free(): invalid pointer" crashes.

Solution: Automatically replace bundled libzstd.so with system version
on Linux. The launcher detects and symlinks to /usr/lib/libzstd.so.1.

- Auto-detect system libzstd at common paths (Arch, Debian, Fedora)
- Backup bundled version as libzstd.so.bundled
- Create symlink to system version
- Add HYTALE_NO_LIBZSTD_FIX=1 to disable if needed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: remove Windows and Linux ARM64 information on the README.md

* Update support_request.yml

* fix: improve update system UX and macOS compatibility

Update System Improvements:
- Fix duplicate update popups by disabling legacy updater.js
- Add skip button to update popup (shows after 30s, on error, or after download)
- Add macOS-specific handling with manual download as primary option
- Add missing open-download-page IPC handler
- Add missing unblockInterface() method to properly clean up after popup close
- Add quitAndInstallUpdate alias in preload for compatibility
- Remove pulse animation when download completes
- Fix manual download button to show correct status and close popup
- Sync player name to settings input after first install

Client Patcher Cleanup:
- Remove server patching code (server uses pre-patched JAR from CDN)
- Simplify to client-only patching
- Remove unused imports (crypto, AdmZip, execSync, spawn, javaManager)
- Remove unused methods (stringToUtf8, findAndReplaceDomainUtf8)
- Move localhost dev code to backup file for reference

Code Quality Fixes:
- Fix duplicate DOMContentLoaded handlers in install.js
- Fix duplicate checkForUpdates definition in preload.js
- Fix redundant if/else in onProgressUpdate callback
- Fix typo "Harwadre" -> "Hardware" in preload.js

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add Russian language support

Added Russian (ru) to the list of available languages.

* chore: drafting documentation on SERVER.md

* Some updates in Russian language localization file

* fix

* Update ru.json

* Fixed Java runtime name and fixed typo

* fixed untranslated place

* Update ru.json

* Update ru.json

* Update ru.json

* Update ru.json

* Update ru.json

* fix: timeout getLatestClient 

fixes #138

* fix: change default version to 7.pwr in main.js

* fix: change default release version to 7.pwr

* fix: change version release to 7.pwr

* docs: Add comprehensive troubleshooting guide (#209)

Add TROUBLESHOOTING.md with solutions for common issues including:

- Windows: Firewall configuration, duplicate mods, SmartScreen
- Linux: GPU detection (NVIDIA/AMD), SDL3_image/libpng dependencies,
  Wayland/X11 issues, Steam Deck support
- macOS: Rosetta 2 for Apple Silicon, code signing, quarantine
- Connection: Server boot failures, regional restrictions
- Authentication: Token errors, config reset procedures
- Avatar/Cosmetics: F2P limitations documentation
- Backup locations for all platforms
- Log locations for bug reports

Solutions compiled from closed GitHub issues (#205, #155, #90, #60,
#144, #192) and community feedback.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* Standardize language codes, improve formatting, and update all locale files. (#224)

* Update German (Germany) localization

* Update Español (España) localization

* Update French (France) localization

* Update Polish (Poland) localization

* Update Portuguese (Brazil) localization

* Update Russian (Russia) localization

* Update Swedish (Sweden) localization

* Update Turkish (Turkey) localization

* Update language codes, names and alphabetical in i18n system

* Changed Spanish language name to the Formal name "Spanish (Spain)"

* Fix PKGBUILD-git

* Fix PKGBUILD

* delete cache after installation

* Enforce 16-char player name limit and update mod sync

Added a maxlength attribute to the player name input and enforced a 16-character limit in both install and settings scripts, providing user feedback if exceeded. Refactored modManager.js to replace symlink-based mod management with a copy-based system, copying enabled mods to HytaleSaves\Mods and removing legacy symlink logic to improve compatibility and avoid permission issues.

* Update installation subtitle

* chore: update quickstart link in README.md

* chore: delete warning of Ubuntu-Debian at Linux Prequisites section

* added featured server list from api

* Add Featured Servers page to GUI

* Update Discord invite URL in client patcher

* Add differential update system

* Remove launcher chat and add Discord popup

* fix: removed 'check disk space' alert on permission file error

* fix: upgrade tar to ^7.5.6 version

* fix: re-add universal arch for mac

* fix: upgrade electron/rebuild to 4.0.3

* fix: removed override tar version

* fix: pkgbuild version to 2.1.2

* fix: src.tar.zst and srcinfo missing files

* feat: add Indonesian language translation

* fix: GPU preference hint to Laptop-only

* feat: create two columns for settings page

* Add Discord invite link to rpc

* docs: add recordings form, fix OS list

* Release v2.2.0

* Release v2.2.0

* Release v2.2.0

* chore: delete icon.ico, moved to build folder

* chore: delete icon.png, moved to build folder

* fix: build and release for tag push-only in release.yml

* fix: gamescope steam deck issue fixes #186 hopefully

* Support branch selection for server patching

* chose: add auto-patch system for pre-release JAR

* fix: preserves arch x64 on linux target for #242

* fix: removed arm64 flags

* fix: redo package.json arch

* update package-lock.json

* Update release.yml

* chore: sync package-lock with package.json

* fix: reorder fedora libzstd paths to first iteration

* feat: enhance gpu detection, drafting

* fix: comprehensive UUID/username persistence bug fixes (#252)

* fix: comprehensive UUID/username persistence bug fixes

Major fixes for UUID/skin reset issues that caused players to lose cosmetics:

Core fixes:
- Username rename now preserves UUID (atomic rename, not new identity)
- Atomic config writes with backup/recovery system
- Case-insensitive UUID lookup with case-preserving storage
- Pre-launch validation blocks play if no username configured
- Removed saveUsername calls from launch/install flows

UUID Modal fixes:
- Fixed isCurrent badge showing on wrong user
- Added switch identity button to change between saved usernames
- Fixed custom UUID input using unsaved DOM username
- UUID list now refreshes when player name changes
- Enabled copy/paste in custom UUID input field

UI/UX improvements:
- Added translation keys for switch username functionality
- CSS user-select fix for UUID input fields
- Allowed Ctrl+V/C/X/A shortcuts in Electron

Files: config.js, gameLauncher.js, gameManager.js, playerManager.js,
launcher.js, settings.js, main.js, preload.js, style.css, en.json

See UUID_BUGS_FIX_PLAN.md for detailed bug list (18 bugs, 16 fixed)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(i18n): add switch username translations to all locales

Added translation keys for username switching functionality:
- notifications.noUsername
- notifications.switchUsernameSuccess
- notifications.switchUsernameFailed
- notifications.playerNameTooLong
- confirm.switchUsernameTitle
- confirm.switchUsernameMessage
- confirm.switchUsernameButton

Languages updated: de-DE, es-ES, fr-FR, id-ID, pl-PL, pt-BR, ru-RU, sv-SE, tr-TR

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: move UUID_BUGS_FIX_PLAN.md to docs folder

* docs: update UUID_BUGS_FIX_PLAN with complete fix details

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* chore: rearrange, fix, and improve README.md

* chore: link downloads, platform, and version to release page in README.md

* chore: update discord link

* chore: insert contact link in CODE_OF_CONDUCT.md

* fix: missing version text on launcher

* chore: update quickstart button link to header

* chore: update discord link and give warning quickstart

* chore revise online play hosting instructions in README

Updated instructions for hosting an online game and clarified troubleshooting steps.

* Fix Turkish translations in tr-TR.json

* fix: EPERM error in Repair Game Button [windows testing needed]

* fix: invalid generated token that caused hangs on exit [windows testing needed]

* fix: major bug - hytale won't launch with laptop machine and ghost processes

* fix: discord RPC destroy error if not connected

* fix: major bug - detach game process to avoid launcher-held handles causing zombie process

* docs: add analysis on ghost process and launcher cleanup

* revert generateLocalTokens, wrong analysis on game launching issue

* revert add deps for generateLocalTokens

* Add proxy client and route downloads through it

* fix: Prevent JAR file corruption during proxy downloads

Fixed binary file corruption when downloading through proxy by using PassThrough stream to preserve data integrity while tracking download progress.

* Improve featured servers layout with Discord integration

- Add Discord button to server cards when discord link is present in API data
- Remove HF2P Servers section to use full width for featured servers
- Increase server card size (300x180px banner, larger fonts and spacing)
- Simplify layout from 2-column grid to single full-width container
- Discord button opens external browser with server invite link

* package version to 2.2.1

Update package.json version from 2.2.0 to 2.2.1 to publish a patch release.

* fix: add game_running_marker to prevent duplicate launches

* Add smart proxy with direct-fallback and logging

* fix: remove duplicate check

* fix: cache invalidation from .env prevents multiple launch attempts

for all env related, it is necessary to clear cache first, otherwise on few launch attempts the game wouldn't run

* fix: redact proxy_url and remove timed out emoji

* Prepare Release v2.2.1

* docs: enhance bug report template with placeholders and options

Updated the bug report template to include placeholders and additional Linux distributions.

* chore revise windows prerequisites and changelog

Updated prerequisites and changelog for version 2.2.1.

* chore: improvise badges, relocate star history, fix discord links

* chore: fix release notes for v2.2.1

* feat(macos): add code signing and notarization support

Add macOS code signing and notarization for Gatekeeper compatibility:

- Add hardened runtime configuration in package.json
- Add entitlements.mac.plist for required app permissions
- Enable built-in electron-builder notarization
- Add code signing and notarization secrets to workflow

Required GitHub Secrets:
- CSC_LINK: Base64-encoded .p12 certificate file
- CSC_KEY_PASSWORD: Password for the .p12 certificate
- APPLE_ID: Apple Developer account email
- APPLE_APP_SPECIFIC_PASSWORD: App-specific password from appleid.apple.com
- APPLE_TEAM_ID: 10-character Apple Developer Team ID

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Revise and enhance Hytale F2P Server Guide

Updated the Hytale F2P Server Guide with new sections and improved formatting.

* Update SERVER.md official accounts info

Added CloudNord hosting information and new section for playing online with official accounts.

* Update SERVER.md

* refactor: replace pre-patched JAR download with ByteBuddy agent

Migrate from downloading pre-patched server JARs from CDN to downloading
the DualAuth ByteBuddy Agent from GitHub releases. The server JAR stays
pristine - auth patching happens at runtime via -javaagent: flag.

clientPatcher.js:
- Replace patchServer() with ensureAgentAvailable()
- Download dualauth-agent.jar to Server/ directory
- Remove serverJarContainsDualAuth() and validateServerJarSize()

gameLauncher.js:
- Set JAVA_TOOL_OPTIONS env var with -javaagent: for runtime patching
- Update logging to show agent status instead of server patch count

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Bump package version to 2.2.2

Update package.json version from 2.2.1 to 2.2.2 to mark a patch release.

* Use new version API and default to v8 PWR

---------

Co-authored-by: Fazri Gading <fazrigading@gmail.com>
Co-authored-by: TalesAmaral <57869141+TalesAmaral@users.noreply.github.com>
Co-authored-by: walti0 <95646872+walti0@users.noreply.github.com>
Co-authored-by: Fazri Gading <super.fai700@gmail.com>
Co-authored-by: sanasol <mail@sanasol.ws>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Terromur <79866197+Terromur@users.noreply.github.com>
Co-authored-by: Zakhar Smokotov <zaharb840@gmail.com>
Co-authored-by: xSamiVS <samtaiebc@gmail.com>
Co-authored-by: MetricsLite <66024355+MetricsLite@users.noreply.github.com>
This commit is contained in:
AMIAY
2026-02-11 11:13:14 +01:00
committed by GitHub
parent ae6a7db80a
commit e5b44341f1
11 changed files with 428 additions and 243 deletions

View File

@@ -1,5 +1,5 @@
const FORCE_CLEAN_INSTALL_VERSION = false;
const CLEAN_INSTALL_TEST_VERSION = '4.pwr';
const CLEAN_INSTALL_TEST_VERSION = 'v4';
module.exports = {
FORCE_CLEAN_INSTALL_VERSION,

View File

@@ -252,8 +252,8 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
if (patchResult.client) {
console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`);
}
if (patchResult.server) {
console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`);
if (patchResult.agent) {
console.log(` Agent: ${patchResult.agent.alreadyExists ? 'already present' : patchResult.agent.success ? 'downloaded' : 'failed'}`);
}
} else {
console.warn('Game patching failed:', patchResult.error);
@@ -408,6 +408,17 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
}
}
// DualAuth Agent: Set JAVA_TOOL_OPTIONS so java picks up -javaagent: flag
// 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}`;
env.JAVA_TOOL_OPTIONS = env.JAVA_TOOL_OPTIONS
? `${env.JAVA_TOOL_OPTIONS} ${agentFlag}`
: agentFlag;
console.log('DualAuth Agent: enabled via JAVA_TOOL_OPTIONS');
}
try {
let spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'],

View File

@@ -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) {
const osName = getOS();
const arch = getArch();
@@ -72,8 +72,23 @@ 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.');
}
const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${branch}/0/${fileName}`;
const dest = path.join(cacheDir, `${branch}_${fileName}`);
const { getPWRUrlFromNewAPI } = require('../services/versionManager');
let url;
let isUsingNewAPI = false;
try {
console.log(`[DownloadPWR] Fetching URL from new API for branch: ${branch}, version: ${fileName}`);
url = await getPWRUrlFromNewAPI(branch, fileName);
isUsingNewAPI = true;
console.log(`[DownloadPWR] Using new API URL: ${url}`);
} catch (error) {
console.error(`[DownloadPWR] Failed to get URL from new API: ${error.message}`);
console.log(`[DownloadPWR] Falling back to old URL format`);
url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${branch}/0/${fileName}.pwr`;
}
const dest = path.join(cacheDir, `${branch}_${fileName}.pwr`);
// Check if file exists and validate it
if (fs.existsSync(dest) && !manualRetry) {
@@ -93,7 +108,7 @@ async function downloadPWR(branch = 'release', fileName = '7.pwr', progressCallb
}
}
console.log('Fetching PWR patch file:', url);
console.log(`Fetching PWR patch file from ${isUsingNewAPI ? 'NEW API' : 'old API'}:`, url);
try {
if (manualRetry) {

View File

@@ -6,32 +6,186 @@ const { smartRequest } = require('../utils/proxyClient');
const BASE_PATCH_URL = 'https://game-patches.hytale.com/patches';
const MANIFEST_API = 'https://files.hytalef2p.com/api/patch_manifest';
const NEW_API_URL = 'https://thecute.cloud/ShipOfYarn/api.php';
async function getLatestClientVersion(branch = 'release') {
let apiCache = null;
let apiCacheTime = 0;
const API_CACHE_DURATION = 60000; // 1 minute
async function fetchNewAPI() {
const now = Date.now();
if (apiCache && (now - apiCacheTime) < API_CACHE_DURATION) {
console.log('[NewAPI] Using cached API data');
return apiCache;
}
try {
console.log(`Fetching latest client version from API (branch: ${branch})...`);
const response = await smartRequest(`https://files.hytalef2p.com/api/version_client?branch=${branch}`, {
timeout: 40000,
console.log('[NewAPI] Fetching from:', NEW_API_URL);
const response = await axios.get(NEW_API_URL, {
timeout: 15000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
if (response.data && response.data.client_version) {
const version = response.data.client_version;
console.log(`Latest client version for ${branch}: ${version}`);
return version;
if (response.data && response.data.hytale) {
apiCache = response.data;
apiCacheTime = now;
console.log('[NewAPI] API data fetched and cached successfully');
return response.data;
} else {
console.log('Warning: Invalid API response, falling back to latest known version (7.pwr)');
return '7.pwr';
throw new Error('Invalid API response structure');
}
} catch (error) {
console.error('Error fetching client version:', error.message);
console.log('Warning: API unavailable, falling back to latest known version (7.pwr)');
return '7.pwr';
console.error('[NewAPI] Error fetching API:', error.message);
if (apiCache) {
console.log('[NewAPI] Using expired cache due to error');
return apiCache;
}
throw error;
}
}
async function getLatestVersionFromNewAPI(branch = 'release') {
try {
const apiData = await fetchNewAPI();
const osName = getOS();
const arch = getArch();
let osKey = osName;
if (osName === 'darwin') {
osKey = 'mac';
}
const branchData = apiData.hytale[branch];
if (!branchData || !branchData[osKey]) {
throw new Error(`No data found for branch: ${branch}, OS: ${osKey}`);
}
const osData = branchData[osKey];
const versions = Object.keys(osData).filter(key => key.endsWith('.pwr'));
if (versions.length === 0) {
throw new Error(`No .pwr files found for ${osKey}`);
}
const versionNumbers = versions.map(v => {
const match = v.match(/v(\d+)/);
return match ? parseInt(match[1]) : 0;
});
const latestVersionNumber = Math.max(...versionNumbers);
console.log(`[NewAPI] Latest version number: ${latestVersionNumber} for branch ${branch}`);
return `v${latestVersionNumber}`;
} catch (error) {
console.error('[NewAPI] Error getting latest version:', error.message);
throw error;
}
}
async function getPWRUrlFromNewAPI(branch = 'release', version = 'v8') {
try {
const apiData = await fetchNewAPI();
const osName = getOS();
const arch = getArch();
let osKey = osName;
if (osName === 'darwin') {
osKey = 'mac';
}
let fileName;
if (osName === 'windows') {
fileName = `${version}-windows-amd64.pwr`;
} else if (osName === 'linux') {
fileName = `${version}-linux-amd64.pwr`;
} else if (osName === 'darwin') {
fileName = `${version}-darwin-arm64.pwr`;
}
const branchData = apiData.hytale[branch];
if (!branchData || !branchData[osKey]) {
throw new Error(`No data found for branch: ${branch}, OS: ${osKey}`);
}
const osData = branchData[osKey];
const url = osData[fileName];
if (!url) {
throw new Error(`No URL found for ${fileName}`);
}
console.log(`[NewAPI] URL for ${fileName}: ${url}`);
return url;
} catch (error) {
console.error('[NewAPI] Error getting PWR URL:', error.message);
throw error;
}
}
async function getLatestClientVersion(branch = 'release') {
try {
console.log(`[NewAPI] Fetching latest client version from new API (branch: ${branch})...`);
// Utiliser la nouvelle API
const latestVersion = await getLatestVersionFromNewAPI(branch);
console.log(`[NewAPI] Latest client version for ${branch}: ${latestVersion}`);
return latestVersion;
} catch (error) {
console.error('[NewAPI] Error fetching client version from new API:', error.message);
console.log('[NewAPI] Falling back to old API...');
// Fallback vers l'ancienne API si la nouvelle échoue
try {
const response = await smartRequest(`https://files.hytalef2p.com/api/version_client?branch=${branch}`, {
timeout: 40000,
headers: {
'User-Agent': 'Hytale-F2P-Launcher'
}
});
if (response.data && response.data.client_version) {
const version = response.data.client_version;
console.log(`Latest client version for ${branch} (old API): ${version}`);
return version;
} else {
console.log('Warning: Invalid API response, falling back to latest known version (v8)');
return 'v8';
}
} catch (fallbackError) {
console.error('Error fetching client version from old API:', fallbackError.message);
console.log('Warning: Both APIs unavailable, falling back to latest known version (v8)');
return 'v8';
}
}
}
// Fonction utilitaire pour extraire le numéro de version
// Supporte les formats: "7.pwr", "v8", "v8-windows-amd64.pwr", etc.
function extractVersionNumber(version) {
if (!version) return 0;
// Nouveau format: "v8" ou "v8-xxx.pwr"
const vMatch = version.match(/v(\d+)/);
if (vMatch) {
return parseInt(vMatch[1]);
}
// Ancien format: "7.pwr"
const pwrMatch = version.match(/(\d+)\.pwr/);
if (pwrMatch) {
return parseInt(pwrMatch[1]);
}
// Fallback: essayer de parser directement
const num = parseInt(version);
return isNaN(num) ? 0 : num;
}
function buildArchiveUrl(buildNumber, branch = 'release') {
const os = getOS();
const arch = getArch();
@@ -50,7 +204,7 @@ async function checkArchiveExists(buildNumber, branch = 'release') {
async function discoverAvailableVersions(latestKnown, branch = 'release', maxProbe = 50) {
const available = [];
const latest = parseInt(latestKnown.replace('.pwr', ''));
const latest = extractVersionNumber(latestKnown);
for (let i = latest; i >= Math.max(1, latest - maxProbe); i--) {
const exists = await checkArchiveExists(i, branch);
@@ -77,7 +231,7 @@ async function fetchPatchManifest(branch = 'release') {
}
async function extractVersionDetails(targetVersion, branch = 'release') {
const buildNumber = parseInt(targetVersion.replace('.pwr', ''));
const buildNumber = extractVersionNumber(targetVersion);
const previousBuild = buildNumber - 1;
const manifest = await fetchPatchManifest(branch);
@@ -103,8 +257,8 @@ function canUseDifferentialUpdate(currentVersion, targetDetails) {
if (!currentVersion) return false;
const currentBuild = parseInt(currentVersion.replace('.pwr', ''));
const expectedSource = parseInt(targetDetails.sourceVersion?.replace('.pwr', '') || '0');
const currentBuild = extractVersionNumber(currentVersion);
const expectedSource = extractVersionNumber(targetDetails.sourceVersion);
return currentBuild === expectedSource;
}
@@ -112,8 +266,8 @@ function canUseDifferentialUpdate(currentVersion, targetDetails) {
function needsIntermediatePatches(currentVersion, targetVersion) {
if (!currentVersion) return [];
const current = parseInt(currentVersion.replace('.pwr', ''));
const target = parseInt(targetVersion.replace('.pwr', ''));
const current = extractVersionNumber(currentVersion);
const target = extractVersionNumber(targetVersion);
const intermediates = [];
for (let i = current + 1; i <= target; i++) {
@@ -160,5 +314,9 @@ module.exports = {
needsIntermediatePatches,
computeFileChecksum,
validateChecksum,
getInstalledClientVersion
getInstalledClientVersion,
fetchNewAPI,
getLatestVersionFromNewAPI,
getPWRUrlFromNewAPI,
extractVersionNumber
};

View File

@@ -7,6 +7,10 @@ const ORIGINAL_DOMAIN = 'hytale.com';
const MIN_DOMAIN_LENGTH = 4;
const MAX_DOMAIN_LENGTH = 16;
// DualAuth ByteBuddy Agent (runtime class transformation, no JAR modification)
const DUALAUTH_AGENT_URL = 'https://github.com/sanasol/hytale-auth-server/releases/latest/download/dualauth-agent.jar';
const DUALAUTH_AGENT_FILENAME = 'dualauth-agent.jar';
function getTargetDomain() {
if (process.env.HYTALE_AUTH_DOMAIN) {
return process.env.HYTALE_AUTH_DOMAIN;
@@ -23,7 +27,7 @@ const DEFAULT_NEW_DOMAIN = 'auth.sanasol.ws';
/**
* Patches HytaleClient binary to replace hytale.com with custom domain
* Server patching is done via pre-patched JAR download from CDN
* Server auth is handled by DualAuth ByteBuddy Agent (-javaagent: flag)
*
* Supports domains from 4 to 16 characters:
* - All F2P traffic routes to single endpoint: https://{domain} (no subdomains)
@@ -494,211 +498,95 @@ class ClientPatcher {
}
/**
* Check if server JAR contains DualAuth classes (was patched)
* Get the path to the DualAuth Agent JAR in a directory
*/
serverJarContainsDualAuth(serverPath) {
try {
const data = fs.readFileSync(serverPath);
// Check for DualAuthContext class signature in JAR
const signature = Buffer.from('DualAuthContext', 'utf8');
return data.includes(signature);
} catch (e) {
return false;
}
getAgentPath(dir) {
return path.join(dir, DUALAUTH_AGENT_FILENAME);
}
/**
* Validate downloaded file is not corrupt/partial
* Server JAR should be at least 50MB
* Download DualAuth ByteBuddy Agent (replaces old pre-patched JAR approach)
* The agent provides runtime class transformation via -javaagent: flag
* No server JAR modification needed - original JAR stays pristine
*/
validateServerJarSize(serverPath) {
try {
const stats = fs.statSync(serverPath);
const minSize = 50 * 1024 * 1024; // 50MB minimum
if (stats.size < minSize) {
console.error(` Downloaded JAR too small: ${(stats.size / 1024 / 1024).toFixed(2)} MB (expected >50MB)`);
return false;
}
console.log(` Downloaded size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
return true;
} catch (e) {
return false;
}
}
async ensureAgentAvailable(serverDir, progressCallback) {
const agentPath = this.getAgentPath(serverDir);
/**
* Patch server JAR by downloading pre-patched version from CDN
*/
async patchServer(serverPath, progressCallback, branch = 'release') {
const newDomain = this.getNewDomain();
console.log('=== DualAuth Agent (ByteBuddy) ===');
console.log(`Target: ${agentPath}`);
console.log('=== Server Patcher (Pre-patched Download) ===');
console.log(`Target: ${serverPath}`);
console.log(`Branch: ${branch}`);
console.log(`Domain: ${newDomain}`);
if (!fs.existsSync(serverPath)) {
const error = `Server JAR not found: ${serverPath}`;
console.error(error);
return { success: false, error };
}
// Check if already patched
const patchFlagFile = serverPath + '.dualauth_patched';
let needsRestore = false;
if (fs.existsSync(patchFlagFile)) {
// Check if agent already exists and is valid
if (fs.existsSync(agentPath)) {
try {
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
if (flagData.domain === newDomain && flagData.branch === branch) {
// Verify JAR actually contains DualAuth classes (game may have auto-updated)
if (this.serverJarContainsDualAuth(serverPath)) {
console.log(`Server already patched for ${newDomain} (${branch}), skipping`);
if (progressCallback) progressCallback('Server already patched', 100);
return { success: true, alreadyPatched: true };
} else {
console.log(' Flag exists but JAR not patched (was auto-updated?), will re-download...');
// Delete stale flag file
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
}
} else {
console.log(`Server patched for "${flagData.domain}" (${flagData.branch}), need to change to "${newDomain}" (${branch})`);
needsRestore = true;
const stats = fs.statSync(agentPath);
if (stats.size > 1024) {
console.log(`DualAuth Agent present (${(stats.size / 1024).toFixed(0)} KB)`);
if (progressCallback) progressCallback('DualAuth Agent ready', 100);
return { success: true, agentPath, alreadyExists: true };
}
// File exists but too small - corrupt, re-download
console.log('Agent file appears corrupt, re-downloading...');
fs.unlinkSync(agentPath);
} catch (e) {
// Flag file corrupt, re-patch
console.log(' Flag file corrupt, will re-download');
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
console.warn('Could not check agent file:', e.message);
}
}
// Restore backup if patched for different domain
if (needsRestore) {
const backupPath = serverPath + '.original';
if (fs.existsSync(backupPath)) {
if (progressCallback) progressCallback('Restoring original for domain change...', 5);
console.log('Restoring original JAR from backup for re-patching...');
fs.copyFileSync(backupPath, serverPath);
if (fs.existsSync(patchFlagFile)) {
fs.unlinkSync(patchFlagFile);
}
} else {
console.warn(' No backup found to restore - will download fresh patched JAR');
}
}
// Create backup
if (progressCallback) progressCallback('Creating backup...', 10);
console.log('Creating backup...');
const backupResult = this.backupClient(serverPath);
if (!backupResult) {
console.warn(' Could not create backup - proceeding without backup');
}
// Only support standard domain (auth.sanasol.ws) via pre-patched download
if (newDomain !== 'auth.sanasol.ws' && newDomain !== 'sanasol.ws') {
console.error(`Domain "${newDomain}" requires DualAuthPatcher - only auth.sanasol.ws is supported via pre-patched download`);
return { success: false, error: `Unsupported domain: ${newDomain}. Only auth.sanasol.ws is supported.` };
}
// Download pre-patched JAR
if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
console.log('Downloading pre-patched HytaleServer.jar...');
// Download agent from GitHub releases
if (progressCallback) progressCallback('Downloading DualAuth Agent...', 20);
console.log(`Downloading from: ${DUALAUTH_AGENT_URL}`);
try {
let url;
if (branch === 'pre-release') {
url = 'https://patcher.authbp.xyz/download/patched_prerelease';
console.log(' Using pre-release patched server from:', url);
} else {
url = 'https://patcher.authbp.xyz/download/patched_release';
console.log(' Using release patched server from:', url);
// Ensure server directory exists
if (!fs.existsSync(serverDir)) {
fs.mkdirSync(serverDir, { recursive: true });
}
const file = fs.createWriteStream(serverPath);
let totalSize = 0;
let downloaded = 0;
const tmpPath = agentPath + '.tmp';
const file = fs.createWriteStream(tmpPath);
const stream = await smartDownloadStream(url, (chunk, downloadedBytes, total) => {
downloaded = downloadedBytes;
totalSize = total;
if (progressCallback && totalSize) {
const percent = 30 + Math.floor((downloaded / totalSize) * 60);
progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent);
const stream = await smartDownloadStream(DUALAUTH_AGENT_URL, (chunk, downloadedBytes, total) => {
if (progressCallback && total) {
const percent = 20 + Math.floor((downloadedBytes / total) * 70);
progressCallback(`Downloading agent... ${(downloadedBytes / 1024).toFixed(0)} KB`, percent);
}
});
stream.pipe(file);
await new Promise((resolve, reject) => {
file.on('finish', () => {
file.close();
resolve();
});
file.on('finish', () => { file.close(); resolve(); });
file.on('error', reject);
stream.on('error', reject);
});
console.log(' Download successful');
// Verify downloaded JAR size and contents
if (progressCallback) progressCallback('Verifying downloaded JAR...', 95);
if (!this.validateServerJarSize(serverPath)) {
console.error('Downloaded JAR appears corrupt or incomplete');
// Restore backup on verification failure
const backupPath = serverPath + '.original';
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, serverPath);
console.log('Restored backup after verification failure');
}
return { success: false, error: 'Downloaded JAR verification failed - file too small (corrupt/partial download)' };
// Verify download
const stats = fs.statSync(tmpPath);
if (stats.size < 1024) {
fs.unlinkSync(tmpPath);
const error = 'Downloaded agent too small (corrupt or failed download)';
console.error(error);
return { success: false, error };
}
if (!this.serverJarContainsDualAuth(serverPath)) {
console.error('Downloaded JAR does not contain DualAuth classes - invalid or corrupt download');
// Restore backup on verification failure
const backupPath = serverPath + '.original';
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, serverPath);
console.log('Restored backup after verification failure');
}
return { success: false, error: 'Downloaded JAR verification failed - missing DualAuth classes' };
// Atomic move
if (fs.existsSync(agentPath)) {
fs.unlinkSync(agentPath);
}
console.log(' Verification successful - DualAuth classes present');
fs.renameSync(tmpPath, agentPath);
// Mark as patched
const sourceUrl = branch === 'pre-release'
? 'https://patcher.authbp.xyz/download/patched_prerelease'
: 'https://patcher.authbp.xyz/download/patched_release';
fs.writeFileSync(patchFlagFile, JSON.stringify({
domain: newDomain,
branch: branch,
patchedAt: new Date().toISOString(),
patcher: 'PrePatchedDownload',
source: sourceUrl
}));
if (progressCallback) progressCallback('Server patching complete', 100);
console.log('=== Server Patching Complete ===');
return { success: true, patchCount: 1 };
console.log(`DualAuth Agent downloaded (${(stats.size / 1024).toFixed(0)} KB)`);
if (progressCallback) progressCallback('DualAuth Agent ready', 100);
return { success: true, agentPath };
} catch (downloadError) {
console.error(`Failed to download patched JAR: ${downloadError.message}`);
// Restore backup on failure
const backupPath = serverPath + '.original';
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, serverPath);
console.log('Restored backup after download failure');
console.error(`Failed to download DualAuth Agent: ${downloadError.message}`);
// Clean up temp file
const tmpPath = agentPath + '.tmp';
if (fs.existsSync(tmpPath)) {
try { fs.unlinkSync(tmpPath); } catch (e) { /* ignore */ }
}
return { success: false, error: `Failed to download patched server: ${downloadError.message}` };
return { success: false, error: downloadError.message };
}
}
@@ -743,12 +631,12 @@ class ClientPatcher {
}
/**
* Ensure both client and server are patched before launching
* Ensure client is patched and DualAuth Agent is available before launching
*/
async ensureClientPatched(gameDir, progressCallback, javaPath = null, branch = 'release') {
const results = {
client: null,
server: null,
agent: null,
success: true
};
@@ -765,22 +653,23 @@ class ClientPatcher {
results.client = { success: false, error: 'Client binary not found' };
}
const serverPath = this.findServerPath(gameDir);
if (serverPath) {
if (progressCallback) progressCallback('Patching server JAR...', 50);
results.server = await this.patchServer(serverPath, (msg, pct) => {
// Download DualAuth ByteBuddy Agent (runtime patching, no JAR modification)
const serverDir = path.join(gameDir, 'Server');
if (fs.existsSync(serverDir)) {
if (progressCallback) progressCallback('Checking DualAuth Agent...', 50);
results.agent = await this.ensureAgentAvailable(serverDir, (msg, pct) => {
if (progressCallback) {
progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
progressCallback(`Agent: ${msg}`, pct ? 50 + pct / 2 : null);
}
}, branch);
});
} else {
console.warn('Could not find HytaleServer.jar');
results.server = { success: false, error: 'Server JAR not found' };
console.warn('Server directory not found, skipping agent download');
results.agent = { success: true, skipped: true };
}
results.success = (results.client && results.client.success) || (results.server && results.server.success);
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched);
results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0);
results.success = (results.client && results.client.success) || (results.agent && results.agent.success);
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.agent && results.agent.alreadyExists);
results.patchCount = results.client ? results.client.patchCount || 0 : 0;
if (progressCallback) progressCallback('Patching complete', 100);