update 2.0.2

This commit is contained in:
AMIAY
2026-01-19 23:15:29 +01:00
parent 300616ba82
commit 21f8527ed4
24 changed files with 3376 additions and 346 deletions

View File

@@ -1,17 +1,105 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { exec } = require('child_process');
const { promisify } = require('util');
const { spawn } = require('child_process');
const { v4: uuidv4 } = require('uuid');
const { getResolvedAppDir, findClientPath } = require('../core/paths');
const { setupWaylandEnvironment } = require('../utils/platformUtils');
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser } = require('../core/config');
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain } = require('../core/config');
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager');
const { updateGameFiles } = require('./gameManager');
// Client patcher for custom auth server (sanasol.ws)
let clientPatcher = null;
try {
clientPatcher = require('../utils/clientPatcher');
} catch (err) {
console.log('[Launcher] Client patcher not available:', err.message);
}
const execAsync = promisify(exec);
// Fetch tokens from the auth server (properly signed with server's Ed25519 key)
async function fetchAuthTokens(uuid, name) {
const authServerUrl = getAuthServerUrl();
try {
console.log(`Fetching auth tokens from ${authServerUrl}/game-session/child`);
const response = await fetch(`${authServerUrl}/game-session/child`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
uuid: uuid,
name: name,
scopes: ['hytale:server', 'hytale:client']
})
});
if (!response.ok) {
throw new Error(`Auth server returned ${response.status}`);
}
const data = await response.json();
console.log('Auth tokens received from server');
return {
identityToken: data.IdentityToken || data.identityToken,
sessionToken: data.SessionToken || data.sessionToken
};
} catch (error) {
console.error('Failed to fetch auth tokens:', error.message);
// Fallback to local generation if server unavailable
return generateLocalTokens(uuid, name);
}
}
// Fallback: Generate tokens locally (won't pass signature validation but allows offline testing)
function generateLocalTokens(uuid, name) {
console.log('Using locally generated tokens (fallback mode)');
const authServerUrl = getAuthServerUrl();
const now = Math.floor(Date.now() / 1000);
const exp = now + 36000;
const header = Buffer.from(JSON.stringify({
alg: 'EdDSA',
kid: '2025-10-01',
typ: 'JWT'
})).toString('base64url');
const identityPayload = Buffer.from(JSON.stringify({
sub: uuid,
name: name,
username: name,
entitlements: ['game.base'],
scope: 'hytale:server hytale:client',
iat: now,
exp: exp,
iss: authServerUrl,
jti: uuidv4()
})).toString('base64url');
const sessionPayload = Buffer.from(JSON.stringify({
sub: uuid,
scope: 'hytale:server',
iat: now,
exp: exp,
iss: authServerUrl,
jti: uuidv4()
})).toString('base64url');
const signature = crypto.randomBytes(64).toString('base64url');
return {
identityToken: `${header}.${identityPayload}.${signature}`,
sessionToken: `${header}.${sessionPayload}.${signature}`
};
}
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
const customAppDir = getResolvedAppDir(installPathOverride);
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
@@ -53,6 +141,51 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
}
}
const uuid = getUuidForUser(playerName);
// Fetch tokens from auth server
if (progressCallback) {
progressCallback('Fetching authentication tokens...', null, null, null, null);
}
const { identityToken, sessionToken } = await fetchAuthTokens(uuid, playerName);
// Patch client and server binaries to use custom auth server (BEFORE signing on macOS)
const authDomain = getAuthDomain();
if (clientPatcher) {
try {
if (progressCallback) {
progressCallback('Patching game for custom server...', null, null, null, null);
}
console.log(`Patching game binaries for ${authDomain}...`);
const patchResult = await clientPatcher.ensureClientPatched(gameLatest, (msg, percent) => {
console.log(`[Patcher] ${msg}`);
if (progressCallback && msg) {
progressCallback(msg, percent, null, null, null);
}
});
if (patchResult.success) {
if (patchResult.alreadyPatched) {
console.log(`Game already patched for ${authDomain}`);
} else {
console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`);
if (patchResult.client) {
console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`);
}
if (patchResult.server) {
console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`);
}
}
} else {
console.warn('Game patching failed:', patchResult.error);
}
} catch (patchError) {
console.warn('Game patching failed (game may not connect to custom server):', patchError.message);
}
}
// macOS: Sign binaries AFTER patching so the patched binaries have valid signatures
if (process.platform === 'darwin') {
try {
const appBundle = path.join(gameLatest, 'Client', 'Hytale.app');
@@ -66,10 +199,10 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
if (fs.existsSync(appBundle)) {
await signPath(appBundle, true);
console.log('Signed macOS app bundle');
console.log('Signed macOS app bundle (after patching)');
} else {
await signPath(path.dirname(clientPath), true);
console.log('Signed macOS client binary');
console.log('Signed macOS client binary (after patching)');
}
if (javaBin && fs.existsSync(javaBin)) {
@@ -85,7 +218,7 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
if (fs.existsSync(serverDir)) {
await execAsync(`xattr -cr "${serverDir}"`).catch(() => {});
await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => {});
console.log('Signed server binaries');
console.log('Signed server binaries (after patching)');
}
if (javaBin && fs.existsSync(javaBin)) {
@@ -113,13 +246,14 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
}
}
const uuid = getUuidForUser(playerName);
const args = [
'--app-dir', gameLatest,
'--java-exec', javaBin,
'--auth-mode', 'offline',
'--auth-mode', 'authenticated',
'--uuid', uuid,
'--name', playerName,
'--identity-token', identityToken,
'--session-token', sessionToken,
'--user-dir', userDataDir
];
@@ -269,4 +403,4 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
module.exports = {
launchGame,
launchGameWithVersionCheck
};
};

View File

@@ -6,7 +6,6 @@ const { getOS, getArch } = require('../utils/platformUtils');
const { downloadFile } = require('../utils/fileManager');
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
const { installButler } = require('./butlerManager');
const { checkAndInstallMultiClient } = require('./multiClientManager');
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig } = require('../core/config');
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
@@ -165,9 +164,6 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
fs.renameSync(tempUpdateDir, gameDir);
const multiResult = await checkAndInstallMultiClient(gameDir, progressCallback);
console.log('Multiplayer-client check result after update:', multiResult);
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
console.log('HomePage.ui update result after update:', homeUIResult);
@@ -318,9 +314,6 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir);
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir);
const multiResult = await checkAndInstallMultiClient(customGameDir, progressCallback);
console.log('Multiplayer check result:', multiResult);
const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
console.log('HomePage.ui update result after installation:', homeUIResult);
@@ -334,8 +327,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
return {
success: true,
installed: true,
multiClient: multiResult
installed: true
};
}

View File

@@ -1,86 +0,0 @@
const fs = require('fs');
const path = require('path');
const { findClientPath } = require('../core/paths');
const { downloadFile } = require('../utils/fileManager');
const { getLatestClientVersion, getMultiClientVersion } = require('../services/versionManager');
async function downloadMultiClient(gameDir, progressCallback) {
try {
if (process.platform !== 'win32') {
console.log('Multiplayer-client is only available for Windows');
return { success: false, reason: 'Platform not supported' };
}
const clientPath = findClientPath(gameDir);
if (!clientPath) {
throw new Error('Game client not found. Install game first.');
}
console.log('Downloading Multiplayer from server...');
if (progressCallback) {
progressCallback('Downloading Multiplayer...', null, null, null, null);
}
const clientUrl = 'http://3.10.208.30:3002/client';
const tempClientPath = path.join(path.dirname(clientPath), 'HytaleClient_temp.exe');
await downloadFile(clientUrl, tempClientPath, progressCallback);
const backupPath = path.join(path.dirname(clientPath), 'HytaleClient_original.exe');
if (!fs.existsSync(backupPath)) {
fs.copyFileSync(clientPath, backupPath);
console.log('Original client backed up');
}
fs.renameSync(tempClientPath, clientPath);
if (progressCallback) {
progressCallback('Multiplayer installed', 100, null, null, null);
}
console.log('Multiplayer installed successfully');
return { success: true, installed: true };
} catch (error) {
console.error('Error installing Multiplayer:', error);
throw new Error(`Failed to install Multiplayer: ${error.message}`);
}
}
async function checkAndInstallMultiClient(gameDir, progressCallback) {
try {
if (process.platform !== 'win32') {
console.log('Multiplayer check skipped (Windows only)');
return { success: true, skipped: true, reason: 'Windows only' };
}
console.log('Checking for Multiplayer availability...');
const [clientVersion, multiVersion] = await Promise.all([
getLatestClientVersion(),
getMultiClientVersion()
]);
if (!multiVersion) {
console.log('Multiplayer not available');
return { success: true, skipped: true, reason: 'Multiplayer not available' };
}
if (clientVersion === multiVersion) {
console.log(`Versions match (${clientVersion}), installing Multiplayer...`);
return await downloadMultiClient(gameDir, progressCallback);
} else {
console.log(`Version mismatch: client=${clientVersion}, multi=${multiVersion}`);
return { success: true, skipped: true, reason: 'Version mismatch' };
}
} catch (error) {
console.error('Error checking Multiplayer:', error);
return { success: false, error: error.message };
}
}
module.exports = {
downloadMultiClient,
checkAndInstallMultiClient
};

View File

@@ -10,7 +10,7 @@ async function downloadAndReplaceHomePageUI(gameDir, progressCallback) {
progressCallback('Downloading HomePage.ui...', null, null, null, null);
}
const homeUIUrl = 'http://3.10.208.30:3002/api/HomeUI';
const homeUIUrl = 'https://files.hytalef2p.com/api/HomeUI';
const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui');
await downloadFile(homeUIUrl, tempHomePath);
@@ -63,7 +63,7 @@ async function downloadAndReplaceLogo(gameDir, progressCallback) {
progressCallback('Downloading Logo@2x.png...', null, null, null, null);
}
const logoUrl = 'http://3.10.208.30:3002/api/Logo';
const logoUrl = 'https://files.hytalef2p.com/api/Logo';
const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png');
await downloadFile(logoUrl, tempLogoPath);