Merge branch 'develop' of https://github.com/amiayweb/Hytale-F2P into develop

This commit is contained in:
Fazri Gading
2026-01-26 00:27:25 +08:00
3 changed files with 123 additions and 79 deletions

View File

@@ -331,6 +331,7 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
} }
}); });
// Monitor game process status in background
setTimeout(() => { setTimeout(() => {
if (!hasExited) { if (!hasExited) {
console.log('Game appears to be running successfully'); console.log('Game appears to be running successfully');
@@ -343,6 +344,7 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
} }
}, 3000); }, 3000);
// Return immediately, don't wait for setTimeout
return { success: true, installed: true, launched: true, pid: child.pid }; return { success: true, installed: true, launched: true, pid: child.pid };
} catch (spawnError) { } catch (spawnError) {
console.error(`Error spawning game process: ${spawnError.message}`); console.error(`Error spawning game process: ${spawnError.message}`);
@@ -404,13 +406,22 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
progressCallback('Launching game...', 80, null, null, null); progressCallback('Launching game...', 80, null, null, null);
} }
return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch); const launchResult = await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
// Ensure we always return a result
if (!launchResult) {
console.error('launchGame returned null/undefined, creating fallback response');
return { success: false, error: 'Game launch failed - no response from launcher' };
}
return launchResult;
} catch (error) { } catch (error) {
console.error('Error in version check and launch:', error); console.error('Error in version check and launch:', error);
if (progressCallback) { if (progressCallback) {
progressCallback(`Error: ${error.message}`, -1, null, null, null); progressCallback(`Error: ${error.message}`, -1, null, null, null);
} }
throw error; // Always return an error response instead of throwing
return { success: false, error: error.message || 'Unknown launch error' };
} }
} }

View File

@@ -525,17 +525,16 @@ class ClientPatcher {
} }
/** /**
* Patch the server JAR using DualAuthPatcher for full dual auth support * Patch the server JAR by downloading pre-patched version
* This uses the same patcher as the Docker server for consistency
* @param {string} serverPath - Path to the HytaleServer.jar * @param {string} serverPath - Path to the HytaleServer.jar
* @param {function} progressCallback - Optional callback for progress updates * @param {function} progressCallback - Optional callback for progress updates
* @param {string} javaPath - Path to Java executable * @param {string} javaPath - Path to Java executable (unused, kept for compatibility)
* @returns {object} Result object with success status and details * @returns {object} Result object with success status and details
*/ */
async patchServer(serverPath, progressCallback, javaPath = null) { async patchServer(serverPath, progressCallback, javaPath = null) {
const newDomain = this.getNewDomain(); const newDomain = this.getNewDomain();
console.log('=== Server Patcher v3.0 (DualAuth) ==='); console.log('=== Server Patcher TEMP SYSTEM NEED TO BE FIXED ===');
console.log(`Target: ${serverPath}`); console.log(`Target: ${serverPath}`);
console.log(`Domain: ${newDomain}`); console.log(`Domain: ${newDomain}`);
@@ -545,13 +544,13 @@ class ClientPatcher {
return { success: false, error }; return { success: false, error };
} }
// Check if already patched with DualAuth // Check if already patched
const patchFlagFile = serverPath + '.dualauth_patched'; const patchFlagFile = serverPath + '.dualauth_patched';
if (fs.existsSync(patchFlagFile)) { if (fs.existsSync(patchFlagFile)) {
try { try {
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8')); const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
if (flagData.domain === newDomain) { if (flagData.domain === newDomain) {
console.log(`Server already patched with DualAuth for ${newDomain}, skipping`); console.log(`Server already patched for ${newDomain}, skipping`);
if (progressCallback) progressCallback('Server already patched', 100); if (progressCallback) progressCallback('Server already patched', 100);
return { success: true, alreadyPatched: true }; return { success: true, alreadyPatched: true };
} }
@@ -560,80 +559,99 @@ class ClientPatcher {
} }
} }
if (progressCallback) progressCallback('Preparing DualAuth patcher...', 10);
// Find Java executable - use bundled JRE first (same as game uses)
const java = javaPath || this.findJava();
if (!java) {
const error = 'Java not found. Please install the game first (it includes Java) or install Java 25 from: https://adoptium.net/';
console.error(error);
return { success: false, error };
}
console.log(`Using Java: ${java}`);
// Setup patcher directory
const patcherDir = path.join(__dirname, '..', 'patcher');
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
const libDir = path.join(patcherDir, 'lib');
// Download patcher from hytale-auth-server if not present
if (progressCallback) progressCallback('Checking patcher...', 15);
try {
await this.ensurePatcherDownloaded(patcherDir);
} catch (e) {
const error = `Failed to download DualAuthPatcher: ${e.message}`;
console.error(error);
return { success: false, error };
}
if (!fs.existsSync(patcherJava)) {
const error = `DualAuthPatcher.java not found at ${patcherJava}`;
console.error(error);
return { success: false, error };
}
// Download ASM libraries if not present
if (progressCallback) progressCallback('Checking ASM libraries...', 20);
await this.ensureAsmLibraries(libDir);
// Compile patcher if needed
if (progressCallback) progressCallback('Compiling patcher...', 30);
const compileResult = await this.compileDualAuthPatcher(java, patcherDir, libDir);
if (!compileResult.success) {
return { success: false, error: compileResult.error };
}
// Create backup // Create backup
if (progressCallback) progressCallback('Creating backup...', 40); if (progressCallback) progressCallback('Creating backup...', 10);
console.log('Creating backup...'); console.log('Creating backup...');
this.backupClient(serverPath); this.backupClient(serverPath);
// Run the patcher // Download pre-patched JAR
if (progressCallback) progressCallback('Patching server JAR...', 50); if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
console.log('Running DualAuthPatcher...'); console.log('Downloading pre-patched HytaleServer.jar from https://files.hytalef2p.com/jar');
const classpath = [ try {
patcherDir, const https = require('https');
path.join(libDir, 'asm-9.6.jar'), const url = 'https://files.hytalef2p.com/jar';
path.join(libDir, 'asm-tree-9.6.jar'),
path.join(libDir, 'asm-util-9.6.jar')
].join(process.platform === 'win32' ? ';' : ':');
const patchResult = await this.runDualAuthPatcher(java, classpath, serverPath, newDomain); await new Promise((resolve, reject) => {
https.get(url, (response) => {
if (response.statusCode === 302 || response.statusCode === 301) {
// Follow redirect
https.get(response.headers.location, (redirectResponse) => {
if (redirectResponse.statusCode !== 200) {
reject(new Error(`Failed to download: HTTP ${redirectResponse.statusCode}`));
return;
}
const file = fs.createWriteStream(serverPath);
const totalSize = parseInt(redirectResponse.headers['content-length'], 10);
let downloaded = 0;
redirectResponse.on('data', (chunk) => {
downloaded += chunk.length;
if (progressCallback && totalSize) {
const percent = 30 + Math.floor((downloaded / totalSize) * 60);
progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent);
}
});
redirectResponse.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}).on('error', reject);
} else if (response.statusCode === 200) {
const file = fs.createWriteStream(serverPath);
const totalSize = parseInt(response.headers['content-length'], 10);
let downloaded = 0;
response.on('data', (chunk) => {
downloaded += chunk.length;
if (progressCallback && totalSize) {
const percent = 30 + Math.floor((downloaded / totalSize) * 60);
progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent);
}
});
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
} else {
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
}
}).on('error', (err) => {
fs.unlink(serverPath, () => {});
reject(err);
});
});
console.log(' Download successful');
if (patchResult.success) {
// Mark as patched // Mark as patched
fs.writeFileSync(patchFlagFile, JSON.stringify({ fs.writeFileSync(patchFlagFile, JSON.stringify({
domain: newDomain, domain: newDomain,
patchedAt: new Date().toISOString(), patchedAt: new Date().toISOString(),
patcher: 'DualAuthPatcher' patcher: 'PrePatchedDownload',
source: 'https://download.sanasol.ws/download/HytaleServer.jar'
})); }));
if (progressCallback) progressCallback('Server patching complete', 100); if (progressCallback) progressCallback('Server patching complete', 100);
console.log('=== Server Patching Complete ==='); console.log('=== Server Patching Complete ===');
return { success: true, patchCount: patchResult.patchCount || 1 }; return { success: true, patchCount: 1 };
} else {
return { success: false, error: patchResult.error }; } 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');
}
return { success: false, error: `Failed to download patched server: ${downloadError.message}` };
} }
} }
@@ -802,10 +820,24 @@ class ClientPatcher {
].join(process.platform === 'win32' ? ';' : ':'); ].join(process.platform === 'win32' ? ';' : ':');
try { try {
execSync(`"${javac}" -cp "${classpath}" -d "${patcherDir}" "${patcherJava}"`, { // Fix PATH for packaged Electron apps on Windows
const execOptions = {
stdio: 'pipe', stdio: 'pipe',
cwd: patcherDir cwd: patcherDir,
}); env: { ...process.env }
};
// Add system32 to PATH for Windows to find cmd.exe
if (process.platform === 'win32') {
const systemRoot = process.env.SystemRoot || 'C:\\WINDOWS';
const systemPath = `${systemRoot}\\system32;${systemRoot};${systemRoot}\\System32\\Wbem`;
execOptions.env.PATH = execOptions.env.PATH
? `${systemPath};${execOptions.env.PATH}`
: systemPath;
execOptions.shell = true;
}
execSync(`"${javac}" -cp "${classpath}" -d "${patcherDir}" "${patcherJava}"`, execOptions);
console.log(' Compilation successful'); console.log(' Compilation successful');
return { success: true }; return { success: true };
} catch (e) { } catch (e) {

View File

@@ -58,11 +58,11 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`); console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`);
if (attempt > 0 && progressCallback) { if (attempt > 0 && progressCallback) {
// Exponential backoff with jitter // Exponential backoff with jitter - longer delays for unstable connections
const baseDelay = 2000; const baseDelay = 3000;
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1); const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
const jitter = Math.random() * 1000; const jitter = Math.random() * 2000;
const delay = Math.min(exponentialDelay + jitter, 30000); const delay = Math.min(exponentialDelay + jitter, 60000);
progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null, retryState); progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null, retryState);
await new Promise(resolve => setTimeout(resolve, delay)); await new Promise(resolve => setTimeout(resolve, delay));
@@ -78,9 +78,9 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
const now = Date.now(); const now = Date.now();
const timeSinceLastProgress = now - lastProgressTime; const timeSinceLastProgress = now - lastProgressTime;
// Only timeout if no data received for 5 minutes (300 seconds) // Only timeout if no data received for 10 minutes (600 seconds) - for very slow connections
if (timeSinceLastProgress > 300000 && hasReceivedData) { if (timeSinceLastProgress > 600000 && hasReceivedData) {
console.log('Download stalled for 5 minutes, aborting...'); console.log('Download stalled for 10 minutes, aborting...');
console.log(`Download had progress before stall: ${(downloaded / 1024 / 1024).toFixed(2)} MB`); console.log(`Download had progress before stall: ${(downloaded / 1024 / 1024).toFixed(2)} MB`);
controller.abort(); controller.abort();
} }
@@ -119,7 +119,7 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
method: 'GET', method: 'GET',
url: url, url: url,
responseType: 'stream', responseType: 'stream',
timeout: 60000, timeout: 120000, // 120 seconds for slow connections
signal: controller.signal, signal: controller.signal,
headers: headers, headers: headers,
validateStatus: function (status) { validateStatus: function (status) {
@@ -403,8 +403,9 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
const retryableErrors = [ const retryableErrors = [
'ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT',
'ESOCKETTIMEDOUT', 'EPROTO', 'ENETDOWN', 'EHOSTUNREACH', 'ESOCKETTIMEDOUT', 'EPROTO', 'ENETDOWN', 'EHOSTUNREACH',
'ECONNABORTED', 'EPIPE', 'ENETRESET', 'EADDRNOTAVAIL',
'ERR_NETWORK', 'ERR_INTERNET_DISCONNECTED', 'ERR_CONNECTION_RESET', 'ERR_NETWORK', 'ERR_INTERNET_DISCONNECTED', 'ERR_CONNECTION_RESET',
'ERR_CONNECTION_TIMED_OUT', 'ERR_NAME_NOT_RESOLVED' 'ERR_CONNECTION_TIMED_OUT', 'ERR_NAME_NOT_RESOLVED', 'ERR_CONNECTION_CLOSED'
]; ];
const isRetryable = retryableErrors.includes(error.code) || const isRetryable = retryableErrors.includes(error.code) ||