mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 09:21:48 -03:00
update 2.0.2
This commit is contained in:
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user