mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 10:31:47 -03:00
fix: Steam Deck/Ubuntu crash with jemalloc allocator
Root cause: glibc 2.41 has stricter heap validation that catches a
pre-existing race condition triggered by binary patching.
Changes:
- Add jemalloc auto-detection and usage on Linux
- Add auto-install via pkexec (graphical sudo prompt)
- Clean up clientPatcher.js (remove debug env vars)
- Add null-padding fix for shorter domain replacements
- Document investigation and solution
The launcher now:
1. Auto-detects jemalloc if installed
2. Offers to auto-install if missing (password prompt)
3. Falls back to MALLOC_CHECK_=0 if jemalloc unavailable
Install manually: sudo pacman -S jemalloc (Arch/Steam Deck)
sudo apt install libjemalloc2 (Debian/Ubuntu)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,57 @@ try {
|
|||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to auto-install jemalloc on Linux using pkexec (graphical sudo)
|
||||||
|
* Returns true if installation was successful
|
||||||
|
*/
|
||||||
|
async function tryInstallJemalloc() {
|
||||||
|
console.log('Linux: Attempting to auto-install jemalloc...');
|
||||||
|
|
||||||
|
// Detect package manager and get install command
|
||||||
|
let installCmd = null;
|
||||||
|
try {
|
||||||
|
await execAsync('which pacman');
|
||||||
|
installCmd = 'pacman -S --noconfirm jemalloc';
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
await execAsync('which apt');
|
||||||
|
installCmd = 'apt install -y libjemalloc2';
|
||||||
|
} catch (e2) {
|
||||||
|
try {
|
||||||
|
await execAsync('which dnf');
|
||||||
|
installCmd = 'dnf install -y jemalloc';
|
||||||
|
} catch (e3) {
|
||||||
|
console.log('Linux: Could not detect package manager for auto-install');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try pkexec first (graphical sudo), fall back to sudo
|
||||||
|
const sudoCommands = ['pkexec', 'sudo'];
|
||||||
|
for (const sudoCmd of sudoCommands) {
|
||||||
|
try {
|
||||||
|
await execAsync(`which ${sudoCmd}`);
|
||||||
|
console.log(`Linux: Installing jemalloc with: ${sudoCmd} ${installCmd}`);
|
||||||
|
await execAsync(`${sudoCmd} ${installCmd}`, { timeout: 120000 });
|
||||||
|
console.log('Linux: jemalloc installed successfully');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
if (e.killed) {
|
||||||
|
console.log('Linux: Install timed out');
|
||||||
|
} else if (e.code === 126 || e.code === 127) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
console.log(`Linux: Install failed with ${sudoCmd}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Linux: Auto-install failed, manual installation required');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch tokens from the auth server (properly signed with server's Ed25519 key)
|
// Fetch tokens from the auth server (properly signed with server's Ed25519 key)
|
||||||
async function fetchAuthTokens(uuid, name) {
|
async function fetchAuthTokens(uuid, name) {
|
||||||
const authServerUrl = getAuthServerUrl();
|
const authServerUrl = getAuthServerUrl();
|
||||||
@@ -285,6 +336,59 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
||||||
Object.assign(env, gpuEnv);
|
Object.assign(env, gpuEnv);
|
||||||
|
|
||||||
|
// Linux: Use jemalloc to fix "free(): invalid pointer" crash on glibc 2.41+ (Steam Deck, Ubuntu LTS)
|
||||||
|
// Root cause: glibc 2.41 has stricter heap validation that catches a pre-existing race condition
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
if (process.env.HYTALE_NO_JEMALLOC !== '1') {
|
||||||
|
const jemallocPaths = [
|
||||||
|
'/usr/lib/libjemalloc.so.2', // Arch Linux, Steam Deck
|
||||||
|
'/usr/lib/x86_64-linux-gnu/libjemalloc.so.2', // Debian/Ubuntu
|
||||||
|
'/usr/lib64/libjemalloc.so.2', // Fedora/RHEL
|
||||||
|
'/usr/lib/libjemalloc.so', // Generic fallback
|
||||||
|
'/usr/lib/x86_64-linux-gnu/libjemalloc.so', // Debian/Ubuntu fallback
|
||||||
|
'/usr/lib64/libjemalloc.so' // Fedora/RHEL fallback
|
||||||
|
];
|
||||||
|
|
||||||
|
let jemalloc = null;
|
||||||
|
for (const p of jemallocPaths) {
|
||||||
|
if (fs.existsSync(p)) {
|
||||||
|
jemalloc = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jemalloc) {
|
||||||
|
env.LD_PRELOAD = jemalloc + (env.LD_PRELOAD ? ':' + env.LD_PRELOAD : '');
|
||||||
|
console.log(`Linux: Using jemalloc allocator for stability (${jemalloc})`);
|
||||||
|
} else {
|
||||||
|
// Try auto-install
|
||||||
|
if (process.env.HYTALE_AUTO_INSTALL_JEMALLOC !== '0') {
|
||||||
|
const installed = await tryInstallJemalloc();
|
||||||
|
if (installed) {
|
||||||
|
for (const p of jemallocPaths) {
|
||||||
|
if (fs.existsSync(p)) {
|
||||||
|
jemalloc = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (jemalloc) {
|
||||||
|
env.LD_PRELOAD = jemalloc + (env.LD_PRELOAD ? ':' + env.LD_PRELOAD : '');
|
||||||
|
console.log(`Linux: Using jemalloc after auto-install (${jemalloc})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!jemalloc) {
|
||||||
|
env.MALLOC_CHECK_ = '0';
|
||||||
|
console.log('Linux: jemalloc not found - install with: sudo pacman -S jemalloc (Arch) or sudo apt install libjemalloc2 (Debian/Ubuntu)');
|
||||||
|
console.log('Linux: Using fallback MALLOC_CHECK_=0 (may still crash on glibc 2.41+)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Linux: jemalloc disabled by HYTALE_NO_JEMALLOC=1');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let spawnOptions = {
|
let spawnOptions = {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
|||||||
@@ -65,15 +65,13 @@ class ClientPatcher {
|
|||||||
*/
|
*/
|
||||||
getDomainStrategy(domain) {
|
getDomainStrategy(domain) {
|
||||||
if (domain.length <= 10) {
|
if (domain.length <= 10) {
|
||||||
// Direct replacement - subdomains will be stripped
|
|
||||||
return {
|
return {
|
||||||
mode: 'direct',
|
mode: 'direct',
|
||||||
mainDomain: domain,
|
mainDomain: domain,
|
||||||
subdomainPrefix: '', // Empty = subdomains stripped
|
subdomainPrefix: '',
|
||||||
description: `Direct replacement: hytale.com -> ${domain}`
|
description: `Direct replacement: hytale.com -> ${domain}`
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Split mode: first 6 chars become subdomain prefix, rest replaces hytale.com
|
|
||||||
const prefix = domain.slice(0, 6);
|
const prefix = domain.slice(0, 6);
|
||||||
const suffix = domain.slice(6);
|
const suffix = domain.slice(6);
|
||||||
return {
|
return {
|
||||||
@@ -88,20 +86,16 @@ class ClientPatcher {
|
|||||||
/**
|
/**
|
||||||
* Convert a string to the length-prefixed byte format used by the client
|
* Convert a string to the length-prefixed byte format used by the client
|
||||||
* Format: [length byte] [00 00 00 padding] [char1] [00] [char2] [00] ... [lastChar]
|
* Format: [length byte] [00 00 00 padding] [char1] [00] [char2] [00] ... [lastChar]
|
||||||
* Note: No null byte after the last character
|
|
||||||
*/
|
*/
|
||||||
stringToLengthPrefixed(str) {
|
stringToLengthPrefixed(str) {
|
||||||
const length = str.length;
|
const length = str.length;
|
||||||
const result = Buffer.alloc(4 + length + (length - 1)); // length byte + padding + chars + separators
|
const result = Buffer.alloc(4 + length + (length - 1));
|
||||||
|
|
||||||
// Length byte
|
|
||||||
result[0] = length;
|
result[0] = length;
|
||||||
// Padding: 00 00 00
|
|
||||||
result[1] = 0x00;
|
result[1] = 0x00;
|
||||||
result[2] = 0x00;
|
result[2] = 0x00;
|
||||||
result[3] = 0x00;
|
result[3] = 0x00;
|
||||||
|
|
||||||
// Characters with null separators (no separator after last char)
|
|
||||||
let pos = 4;
|
let pos = 4;
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
result[pos++] = str.charCodeAt(i);
|
result[pos++] = str.charCodeAt(i);
|
||||||
@@ -147,8 +141,8 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace bytes in buffer - only overwrites the length of new bytes
|
* Replace bytes in buffer with null-padding for shorter replacements
|
||||||
* Prevents offset corruption by not expanding the replacement
|
* When new pattern is shorter than old, pads with 0x00 to prevent leftover bytes
|
||||||
*/
|
*/
|
||||||
replaceBytes(buffer, oldBytes, newBytes) {
|
replaceBytes(buffer, oldBytes, newBytes) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -162,7 +156,10 @@ class ClientPatcher {
|
|||||||
const positions = this.findAllOccurrences(result, oldBytes);
|
const positions = this.findAllOccurrences(result, oldBytes);
|
||||||
|
|
||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
// Only overwrite the length of the new bytes
|
// Zero-fill the old region first if new is shorter (prevents leftover bytes)
|
||||||
|
if (newBytes.length < oldBytes.length) {
|
||||||
|
result.fill(0x00, pos, pos + oldBytes.length);
|
||||||
|
}
|
||||||
newBytes.copy(result, pos);
|
newBytes.copy(result, pos);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
@@ -171,8 +168,7 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UTF-8 domain replacement for Java JAR files.
|
* UTF-8 domain replacement for Java JAR files
|
||||||
* Java stores strings in UTF-8 format in the constant pool.
|
|
||||||
*/
|
*/
|
||||||
findAndReplaceDomainUtf8(data, oldDomain, newDomain) {
|
findAndReplaceDomainUtf8(data, oldDomain, newDomain) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -186,22 +182,23 @@ class ClientPatcher {
|
|||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
newUtf8.copy(result, pos);
|
newUtf8.copy(result, pos);
|
||||||
count++;
|
count++;
|
||||||
console.log(` Patched UTF-8 occurrence at offset 0x${pos.toString(16)}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { buffer: result, count };
|
return { buffer: result, count };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Smart domain replacement that handles both null-terminated and non-null-terminated strings.
|
* Smart domain replacement for .NET AOT binaries
|
||||||
* .NET AOT stores some strings in various formats:
|
|
||||||
* - Standard UTF-16LE (each char is 2 bytes with \x00 high byte)
|
|
||||||
* - Length-prefixed where last char may have metadata byte instead of \x00
|
|
||||||
*/
|
*/
|
||||||
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
|
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const result = Buffer.from(data);
|
const result = Buffer.from(data);
|
||||||
|
|
||||||
|
if (newDomain.length > oldDomain.length) {
|
||||||
|
console.warn(` Warning: New domain longer than old, skipping smart replacement`);
|
||||||
|
return { buffer: result, count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1));
|
const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1));
|
||||||
const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1));
|
const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1));
|
||||||
|
|
||||||
@@ -217,18 +214,13 @@ class ClientPatcher {
|
|||||||
const lastCharFirstByte = result[lastCharPos];
|
const lastCharFirstByte = result[lastCharPos];
|
||||||
|
|
||||||
if (lastCharFirstByte === oldLastCharByte) {
|
if (lastCharFirstByte === oldLastCharByte) {
|
||||||
newUtf16NoLast.copy(result, pos);
|
// Zero-fill first if new is shorter
|
||||||
|
if (newUtf16NoLast.length < oldUtf16NoLast.length) {
|
||||||
result[lastCharPos] = newLastCharByte;
|
result.fill(0x00, pos, pos + oldUtf16NoLast.length);
|
||||||
|
|
||||||
if (lastCharPos + 1 < result.length) {
|
|
||||||
const secondByte = result[lastCharPos + 1];
|
|
||||||
if (secondByte === 0x00) {
|
|
||||||
console.log(` Patched UTF-16LE occurrence at offset 0x${pos.toString(16)}`);
|
|
||||||
} else {
|
|
||||||
console.log(` Patched length-prefixed occurrence at offset 0x${pos.toString(16)} (metadata: 0x${secondByte.toString(16)})`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newUtf16NoLast.copy(result, pos);
|
||||||
|
result[lastCharPos] = newLastCharByte;
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,7 +230,6 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply all domain patches using length-prefixed format
|
* Apply all domain patches using length-prefixed format
|
||||||
* This is the main patching method for variable-length domains
|
|
||||||
*/
|
*/
|
||||||
applyDomainPatches(data, domain, protocol = 'https://') {
|
applyDomainPatches(data, domain, protocol = 'https://') {
|
||||||
let result = Buffer.from(data);
|
let result = Buffer.from(data);
|
||||||
@@ -251,28 +242,26 @@ class ClientPatcher {
|
|||||||
const oldSentry = 'https://ca900df42fcf57d4dd8401a86ddd7da2@sentry.hytale.com/2';
|
const oldSentry = 'https://ca900df42fcf57d4dd8401a86ddd7da2@sentry.hytale.com/2';
|
||||||
const newSentry = `${protocol}t@${domain}/2`;
|
const newSentry = `${protocol}t@${domain}/2`;
|
||||||
|
|
||||||
console.log(` Patching sentry: ${oldSentry.slice(0, 30)}... -> ${newSentry}`);
|
|
||||||
const sentryResult = this.replaceBytes(
|
const sentryResult = this.replaceBytes(
|
||||||
result,
|
result,
|
||||||
this.stringToLengthPrefixed(oldSentry),
|
this.stringToLengthPrefixed(oldSentry),
|
||||||
this.stringToLengthPrefixed(newSentry)
|
this.stringToLengthPrefixed(newSentry)
|
||||||
);
|
);
|
||||||
result = sentryResult.buffer;
|
result = sentryResult.buffer;
|
||||||
if (sentryResult.count > 0) {
|
if (sentryResult.count > 0) {
|
||||||
console.log(` Replaced ${sentryResult.count} sentry occurrence(s)`);
|
console.log(` Patched ${sentryResult.count} sentry URL(s)`);
|
||||||
totalCount += sentryResult.count;
|
totalCount += sentryResult.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Patch main domain (hytale.com -> mainDomain)
|
// 2. Patch main domain (hytale.com -> mainDomain)
|
||||||
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
|
|
||||||
const domainResult = this.replaceBytes(
|
const domainResult = this.replaceBytes(
|
||||||
result,
|
result,
|
||||||
this.stringToLengthPrefixed(ORIGINAL_DOMAIN),
|
this.stringToLengthPrefixed(ORIGINAL_DOMAIN),
|
||||||
this.stringToLengthPrefixed(strategy.mainDomain)
|
this.stringToLengthPrefixed(strategy.mainDomain)
|
||||||
);
|
);
|
||||||
result = domainResult.buffer;
|
result = domainResult.buffer;
|
||||||
if (domainResult.count > 0) {
|
if (domainResult.count > 0) {
|
||||||
console.log(` Replaced ${domainResult.count} domain occurrence(s)`);
|
console.log(` Patched ${domainResult.count} domain occurrence(s)`);
|
||||||
totalCount += domainResult.count;
|
totalCount += domainResult.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,15 +270,14 @@ class ClientPatcher {
|
|||||||
const newSubdomainPrefix = protocol + strategy.subdomainPrefix;
|
const newSubdomainPrefix = protocol + strategy.subdomainPrefix;
|
||||||
|
|
||||||
for (const sub of subdomains) {
|
for (const sub of subdomains) {
|
||||||
console.log(` Patching subdomain: ${sub} -> ${newSubdomainPrefix}`);
|
|
||||||
const subResult = this.replaceBytes(
|
const subResult = this.replaceBytes(
|
||||||
result,
|
result,
|
||||||
this.stringToLengthPrefixed(sub),
|
this.stringToLengthPrefixed(sub),
|
||||||
this.stringToLengthPrefixed(newSubdomainPrefix)
|
this.stringToLengthPrefixed(newSubdomainPrefix)
|
||||||
);
|
);
|
||||||
result = subResult.buffer;
|
result = subResult.buffer;
|
||||||
if (subResult.count > 0) {
|
if (subResult.count > 0) {
|
||||||
console.log(` Replaced ${subResult.count} occurrence(s)`);
|
console.log(` Patched ${subResult.count} ${sub} occurrence(s)`);
|
||||||
totalCount += subResult.count;
|
totalCount += subResult.count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,50 +285,13 @@ class ClientPatcher {
|
|||||||
return { buffer: result, count: totalCount };
|
return { buffer: result, count: totalCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7
|
|
||||||
*/
|
|
||||||
patchDiscordUrl(data) {
|
|
||||||
let count = 0;
|
|
||||||
const result = Buffer.from(data);
|
|
||||||
|
|
||||||
const oldUrl = '.gg/hytale';
|
|
||||||
const newUrl = '.gg/MHkEjepMQ7';
|
|
||||||
|
|
||||||
// Try length-prefixed format first
|
|
||||||
const lpResult = this.replaceBytes(
|
|
||||||
result,
|
|
||||||
this.stringToLengthPrefixed(oldUrl),
|
|
||||||
this.stringToLengthPrefixed(newUrl)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (lpResult.count > 0) {
|
|
||||||
return { buffer: lpResult.buffer, count: lpResult.count };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to UTF-16LE
|
|
||||||
const oldUtf16 = this.stringToUtf16LE(oldUrl);
|
|
||||||
const newUtf16 = this.stringToUtf16LE(newUrl);
|
|
||||||
|
|
||||||
const positions = this.findAllOccurrences(result, oldUtf16);
|
|
||||||
|
|
||||||
for (const pos of positions) {
|
|
||||||
newUtf16.copy(result, pos);
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { buffer: result, count };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the client binary has already been patched
|
* Check if the client binary has already been patched
|
||||||
* Also verifies the binary actually contains the patched domain
|
|
||||||
*/
|
*/
|
||||||
isPatchedAlready(clientPath) {
|
isPatchedAlready(clientPath) {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
const patchFlagFile = clientPath + this.patchedFlag;
|
const patchFlagFile = clientPath + this.patchedFlag;
|
||||||
|
|
||||||
// First check flag file
|
|
||||||
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'));
|
||||||
@@ -358,7 +309,7 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Flag file corrupt or unreadable
|
// Flag file corrupt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -378,8 +329,7 @@ class ClientPatcher {
|
|||||||
patchMode: strategy.mode,
|
patchMode: strategy.mode,
|
||||||
mainDomain: strategy.mainDomain,
|
mainDomain: strategy.mainDomain,
|
||||||
subdomainPrefix: strategy.subdomainPrefix,
|
subdomainPrefix: strategy.subdomainPrefix,
|
||||||
patcherVersion: '2.0.0',
|
patcherVersion: '2.1.0'
|
||||||
verified: 'binary_contents'
|
|
||||||
};
|
};
|
||||||
fs.writeFileSync(patchFlagFile, JSON.stringify(flagData, null, 2));
|
fs.writeFileSync(patchFlagFile, JSON.stringify(flagData, null, 2));
|
||||||
}
|
}
|
||||||
@@ -395,12 +345,10 @@ class ClientPatcher {
|
|||||||
return backupPath;
|
return backupPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if current file differs from backup (might have been updated)
|
|
||||||
const currentSize = fs.statSync(clientPath).size;
|
const currentSize = fs.statSync(clientPath).size;
|
||||||
const backupSize = fs.statSync(backupPath).size;
|
const backupSize = fs.statSync(backupPath).size;
|
||||||
|
|
||||||
if (currentSize !== backupSize) {
|
if (currentSize !== backupSize) {
|
||||||
// File was updated, create timestamped backup of old backup
|
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||||
const oldBackupPath = `${clientPath}.original.${timestamp}`;
|
const oldBackupPath = `${clientPath}.original.${timestamp}`;
|
||||||
console.log(` File updated, archiving old backup to ${path.basename(oldBackupPath)}`);
|
console.log(` File updated, archiving old backup to ${path.basename(oldBackupPath)}`);
|
||||||
@@ -433,22 +381,15 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch the client binary to use the custom domain
|
* Patch the client binary to use the custom domain
|
||||||
* @param {string} clientPath - Path to the HytaleClient binary
|
|
||||||
* @param {function} progressCallback - Optional callback for progress updates
|
|
||||||
* @returns {object} Result object with success status and details
|
|
||||||
*/
|
*/
|
||||||
async patchClient(clientPath, progressCallback) {
|
async patchClient(clientPath, progressCallback) {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
const strategy = this.getDomainStrategy(newDomain);
|
const strategy = this.getDomainStrategy(newDomain);
|
||||||
|
|
||||||
console.log('=== Client Patcher v2.0 ===');
|
console.log('=== Client Patcher v2.1 ===');
|
||||||
console.log(`Target: ${clientPath}`);
|
console.log(`Target: ${clientPath}`);
|
||||||
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
||||||
console.log(`Mode: ${strategy.mode}`);
|
console.log(`Mode: ${strategy.mode}`);
|
||||||
if (strategy.mode === 'split') {
|
|
||||||
console.log(` Subdomain prefix: ${strategy.subdomainPrefix}`);
|
|
||||||
console.log(` Main domain: ${strategy.mainDomain}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(clientPath)) {
|
if (!fs.existsSync(clientPath)) {
|
||||||
const error = `Client binary not found: ${clientPath}`;
|
const error = `Client binary not found: ${clientPath}`;
|
||||||
@@ -458,41 +399,29 @@ class ClientPatcher {
|
|||||||
|
|
||||||
if (this.isPatchedAlready(clientPath)) {
|
if (this.isPatchedAlready(clientPath)) {
|
||||||
console.log(`Client already patched for ${newDomain}, skipping`);
|
console.log(`Client already patched for ${newDomain}, skipping`);
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Client already patched', 100);
|
||||||
progressCallback('Client already patched', 100);
|
|
||||||
}
|
|
||||||
return { success: true, alreadyPatched: true, patchCount: 0 };
|
return { success: true, alreadyPatched: true, patchCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Preparing to patch client...', 10);
|
||||||
progressCallback('Preparing to patch client...', 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Creating backup...');
|
console.log('Creating backup...');
|
||||||
this.backupClient(clientPath);
|
this.backupClient(clientPath);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Reading client binary...', 20);
|
||||||
progressCallback('Reading client binary...', 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Reading client binary...');
|
console.log('Reading client binary...');
|
||||||
const data = fs.readFileSync(clientPath);
|
const data = fs.readFileSync(clientPath);
|
||||||
console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`);
|
console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching domain references...', 50);
|
||||||
progressCallback('Patching domain references...', 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Applying domain patches (length-prefixed format)...');
|
console.log('Applying domain patches...');
|
||||||
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
||||||
|
|
||||||
console.log('Patching Discord URLs...');
|
if (count === 0) {
|
||||||
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
|
// Try legacy UTF-16LE format
|
||||||
|
|
||||||
if (count === 0 && discordCount === 0) {
|
|
||||||
console.log('No occurrences found - trying legacy UTF-16LE format...');
|
console.log('No occurrences found - trying legacy UTF-16LE format...');
|
||||||
|
|
||||||
// Fallback to legacy patching for older binary formats
|
|
||||||
const legacyResult = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
const legacyResult = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
||||||
if (legacyResult.count > 0) {
|
if (legacyResult.count > 0) {
|
||||||
console.log(`Found ${legacyResult.count} occurrences with legacy format`);
|
console.log(`Found ${legacyResult.count} occurrences with legacy format`);
|
||||||
@@ -501,40 +430,31 @@ class ClientPatcher {
|
|||||||
return { success: true, patchCount: legacyResult.count, format: 'legacy' };
|
return { success: true, patchCount: legacyResult.count, format: 'legacy' };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('No occurrences found - binary may already be modified or has different format');
|
console.log('No occurrences found - binary may already be modified');
|
||||||
return { success: true, patchCount: 0, warning: 'No occurrences found' };
|
return { success: true, patchCount: 0, warning: 'No occurrences found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Writing patched binary...', 80);
|
||||||
progressCallback('Writing patched binary...', 80);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Writing patched binary...');
|
console.log('Writing patched binary...');
|
||||||
fs.writeFileSync(clientPath, finalData);
|
fs.writeFileSync(clientPath, patchedData);
|
||||||
|
|
||||||
this.markAsPatched(clientPath);
|
this.markAsPatched(clientPath);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching complete', 100);
|
||||||
progressCallback('Patching complete', 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Successfully patched ${count} domain occurrences and ${discordCount} Discord URLs`);
|
console.log(`Successfully patched ${count} occurrences`);
|
||||||
console.log('=== Patching Complete ===');
|
console.log('=== Patching Complete ===');
|
||||||
|
|
||||||
return { success: true, patchCount: count + discordCount };
|
return { success: true, patchCount: count };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch the server JAR by downloading pre-patched version
|
* Patch the server JAR by downloading pre-patched version
|
||||||
* @param {string} serverPath - Path to the HytaleServer.jar
|
|
||||||
* @param {function} progressCallback - Optional callback for progress updates
|
|
||||||
* @param {string} javaPath - Path to Java executable (unused, kept for compatibility)
|
|
||||||
* @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 TEMP SYSTEM NEED TO BE FIXED ===');
|
console.log('=== Server Patcher ===');
|
||||||
console.log(`Target: ${serverPath}`);
|
console.log(`Target: ${serverPath}`);
|
||||||
console.log(`Domain: ${newDomain}`);
|
console.log(`Domain: ${newDomain}`);
|
||||||
|
|
||||||
@@ -544,7 +464,6 @@ class ClientPatcher {
|
|||||||
return { success: false, error };
|
return { success: false, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if already patched
|
|
||||||
const patchFlagFile = serverPath + '.dualauth_patched';
|
const patchFlagFile = serverPath + '.dualauth_patched';
|
||||||
if (fs.existsSync(patchFlagFile)) {
|
if (fs.existsSync(patchFlagFile)) {
|
||||||
try {
|
try {
|
||||||
@@ -555,16 +474,14 @@ class ClientPatcher {
|
|||||||
return { success: true, alreadyPatched: true };
|
return { success: true, alreadyPatched: true };
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Flag file corrupt, re-patch
|
// Re-patch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create backup
|
|
||||||
if (progressCallback) progressCallback('Creating backup...', 10);
|
if (progressCallback) progressCallback('Creating backup...', 10);
|
||||||
console.log('Creating backup...');
|
console.log('Creating backup...');
|
||||||
this.backupClient(serverPath);
|
this.backupClient(serverPath);
|
||||||
|
|
||||||
// Download pre-patched JAR
|
|
||||||
if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
|
if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
|
||||||
console.log('Downloading pre-patched HytaleServer.jar');
|
console.log('Downloading pre-patched HytaleServer.jar');
|
||||||
|
|
||||||
@@ -573,55 +490,37 @@ class ClientPatcher {
|
|||||||
const url = 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar';
|
const url = 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar';
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
https.get(url, (response) => {
|
const handleResponse = (response) => {
|
||||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||||
// Follow redirect
|
https.get(response.headers.location, handleResponse).on('error', reject);
|
||||||
https.get(response.headers.location, (redirectResponse) => {
|
return;
|
||||||
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) => {
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
reject(new Error(`HTTP ${response.statusCode}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
https.get(url, handleResponse).on('error', (err) => {
|
||||||
fs.unlink(serverPath, () => {});
|
fs.unlink(serverPath, () => {});
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
@@ -629,12 +528,11 @@ class ClientPatcher {
|
|||||||
|
|
||||||
console.log(' Download successful');
|
console.log(' Download successful');
|
||||||
|
|
||||||
// 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: 'PrePatchedDownload',
|
patcher: 'PrePatchedDownload',
|
||||||
source: 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar'
|
source: url
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (progressCallback) progressCallback('Server patching complete', 100);
|
if (progressCallback) progressCallback('Server patching complete', 100);
|
||||||
@@ -643,8 +541,7 @@ class ClientPatcher {
|
|||||||
|
|
||||||
} catch (downloadError) {
|
} catch (downloadError) {
|
||||||
console.error(`Failed to download patched JAR: ${downloadError.message}`);
|
console.error(`Failed to download patched JAR: ${downloadError.message}`);
|
||||||
|
|
||||||
// Restore backup on failure
|
|
||||||
const backupPath = serverPath + '.original';
|
const backupPath = serverPath + '.original';
|
||||||
if (fs.existsSync(backupPath)) {
|
if (fs.existsSync(backupPath)) {
|
||||||
fs.copyFileSync(backupPath, serverPath);
|
fs.copyFileSync(backupPath, serverPath);
|
||||||
@@ -656,288 +553,38 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find Java executable - uses bundled JRE first (same as game uses)
|
* Find Java executable
|
||||||
* Falls back to system Java if bundled not available
|
|
||||||
*/
|
*/
|
||||||
findJava() {
|
findJava() {
|
||||||
// 1. Try bundled JRE first (comes with the game)
|
|
||||||
try {
|
try {
|
||||||
const bundled = getBundledJavaPath(JRE_DIR);
|
const bundled = getBundledJavaPath(JRE_DIR);
|
||||||
if (bundled && fs.existsSync(bundled)) {
|
if (bundled && fs.existsSync(bundled)) {
|
||||||
console.log(`Using bundled Java: ${bundled}`);
|
|
||||||
return bundled;
|
return bundled;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
// Bundled not available
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Try javaManager's getJavaExec (handles all fallbacks)
|
|
||||||
try {
|
try {
|
||||||
const javaExec = getJavaExec(JRE_DIR);
|
const javaExec = getJavaExec(JRE_DIR);
|
||||||
if (javaExec && fs.existsSync(javaExec)) {
|
if (javaExec && fs.existsSync(javaExec)) {
|
||||||
console.log(`Using Java from javaManager: ${javaExec}`);
|
|
||||||
return javaExec;
|
return javaExec;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
// Not available
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Check JAVA_HOME
|
|
||||||
if (process.env.JAVA_HOME) {
|
if (process.env.JAVA_HOME) {
|
||||||
const javaHome = process.env.JAVA_HOME;
|
const javaBin = path.join(process.env.JAVA_HOME, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
|
||||||
const javaBin = path.join(javaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
|
|
||||||
if (fs.existsSync(javaBin)) {
|
if (fs.existsSync(javaBin)) {
|
||||||
console.log(`Using Java from JAVA_HOME: ${javaBin}`);
|
|
||||||
return javaBin;
|
return javaBin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Try 'java' from PATH
|
|
||||||
try {
|
try {
|
||||||
execSync('java -version 2>&1', { encoding: 'utf8' });
|
execSync('java -version 2>&1', { encoding: 'utf8' });
|
||||||
console.log('Using Java from PATH');
|
|
||||||
return 'java';
|
return 'java';
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
// Not in PATH
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Download DualAuthPatcher from hytale-auth-server if not present
|
|
||||||
*/
|
|
||||||
async ensurePatcherDownloaded(patcherDir) {
|
|
||||||
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
|
|
||||||
const patcherUrl = 'https://raw.githubusercontent.com/sanasol/hytale-auth-server/master/patcher/DualAuthPatcher.java';
|
|
||||||
|
|
||||||
if (!fs.existsSync(patcherDir)) {
|
|
||||||
fs.mkdirSync(patcherDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(patcherJava)) {
|
|
||||||
console.log('Downloading DualAuthPatcher from hytale-auth-server...');
|
|
||||||
try {
|
|
||||||
const https = require('https');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const file = fs.createWriteStream(patcherJava);
|
|
||||||
https.get(patcherUrl, (response) => {
|
|
||||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
||||||
// Follow redirect
|
|
||||||
https.get(response.headers.location, (redirectResponse) => {
|
|
||||||
redirectResponse.pipe(file);
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}).on('error', reject);
|
|
||||||
} else {
|
|
||||||
response.pipe(file);
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).on('error', (err) => {
|
|
||||||
fs.unlink(patcherJava, () => {});
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
console.log(' Downloaded DualAuthPatcher.java');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(` Failed to download DualAuthPatcher: ${e.message}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download ASM libraries if not present
|
|
||||||
*/
|
|
||||||
async ensureAsmLibraries(libDir) {
|
|
||||||
if (!fs.existsSync(libDir)) {
|
|
||||||
fs.mkdirSync(libDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const libs = [
|
|
||||||
{ name: 'asm-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar' },
|
|
||||||
{ name: 'asm-tree-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar' },
|
|
||||||
{ name: 'asm-util-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-util/9.6/asm-util-9.6.jar' }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const lib of libs) {
|
|
||||||
const libPath = path.join(libDir, lib.name);
|
|
||||||
if (!fs.existsSync(libPath)) {
|
|
||||||
console.log(`Downloading ${lib.name}...`);
|
|
||||||
try {
|
|
||||||
const https = require('https');
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const file = fs.createWriteStream(libPath);
|
|
||||||
https.get(lib.url, (response) => {
|
|
||||||
response.pipe(file);
|
|
||||||
file.on('finish', () => {
|
|
||||||
file.close();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}).on('error', (err) => {
|
|
||||||
fs.unlink(libPath, () => {});
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
console.log(` Downloaded ${lib.name}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(` Failed to download ${lib.name}: ${e.message}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile DualAuthPatcher if needed
|
|
||||||
*/
|
|
||||||
async compileDualAuthPatcher(java, patcherDir, libDir) {
|
|
||||||
const patcherClass = path.join(patcherDir, 'DualAuthPatcher.class');
|
|
||||||
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
|
|
||||||
|
|
||||||
// Check if already compiled and up to date
|
|
||||||
if (fs.existsSync(patcherClass)) {
|
|
||||||
const classTime = fs.statSync(patcherClass).mtime;
|
|
||||||
const javaTime = fs.statSync(patcherJava).mtime;
|
|
||||||
if (classTime > javaTime) {
|
|
||||||
console.log('DualAuthPatcher already compiled');
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Compiling DualAuthPatcher...');
|
|
||||||
|
|
||||||
const javac = java.replace(/java(\.exe)?$/, 'javac$1');
|
|
||||||
const classpath = [
|
|
||||||
path.join(libDir, 'asm-9.6.jar'),
|
|
||||||
path.join(libDir, 'asm-tree-9.6.jar'),
|
|
||||||
path.join(libDir, 'asm-util-9.6.jar')
|
|
||||||
].join(process.platform === 'win32' ? ';' : ':');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Fix PATH for packaged Electron apps on Windows
|
|
||||||
const execOptions = {
|
|
||||||
stdio: 'pipe',
|
|
||||||
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');
|
|
||||||
return { success: true };
|
|
||||||
} catch (e) {
|
|
||||||
const error = `Failed to compile DualAuthPatcher: ${e.message}`;
|
|
||||||
console.error(error);
|
|
||||||
if (e.stderr) console.error(e.stderr.toString());
|
|
||||||
return { success: false, error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run DualAuthPatcher on the server JAR
|
|
||||||
*/
|
|
||||||
async runDualAuthPatcher(java, classpath, serverPath, domain) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const args = ['-cp', classpath, 'DualAuthPatcher', serverPath];
|
|
||||||
const env = { ...process.env, HYTALE_AUTH_DOMAIN: domain };
|
|
||||||
|
|
||||||
console.log(`Running: java ${args.join(' ')}`);
|
|
||||||
console.log(` HYTALE_AUTH_DOMAIN=${domain}`);
|
|
||||||
|
|
||||||
const proc = spawn(java, args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
||||||
|
|
||||||
let stdout = '';
|
|
||||||
let stderr = '';
|
|
||||||
|
|
||||||
proc.stdout.on('data', (data) => {
|
|
||||||
const str = data.toString();
|
|
||||||
stdout += str;
|
|
||||||
console.log(str.trim());
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.stderr.on('data', (data) => {
|
|
||||||
const str = data.toString();
|
|
||||||
stderr += str;
|
|
||||||
console.error(str.trim());
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('close', (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
resolve({ success: true, stdout });
|
|
||||||
} else {
|
|
||||||
resolve({ success: false, error: `Patcher exited with code ${code}: ${stderr}` });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('error', (err) => {
|
|
||||||
resolve({ success: false, error: `Failed to run patcher: ${err.message}` });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy server patcher (simple domain replacement, no dual auth)
|
|
||||||
* Use patchServer() for full dual auth support
|
|
||||||
*/
|
|
||||||
async patchServerLegacy(serverPath, progressCallback) {
|
|
||||||
const newDomain = this.getNewDomain();
|
|
||||||
const strategy = this.getDomainStrategy(newDomain);
|
|
||||||
|
|
||||||
console.log('=== Legacy Server Patcher ===');
|
|
||||||
console.log(`Target: ${serverPath}`);
|
|
||||||
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
|
||||||
|
|
||||||
if (!fs.existsSync(serverPath)) {
|
|
||||||
return { success: false, error: `Server JAR not found: ${serverPath}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) progressCallback('Patching server...', 20);
|
|
||||||
|
|
||||||
console.log('Opening server JAR...');
|
|
||||||
const zip = new AdmZip(serverPath);
|
|
||||||
const entries = zip.getEntries();
|
|
||||||
|
|
||||||
let totalCount = 0;
|
|
||||||
const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN);
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const name = entry.entryName;
|
|
||||||
if (name.endsWith('.class') || name.endsWith('.properties') ||
|
|
||||||
name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) {
|
|
||||||
const data = entry.getData();
|
|
||||||
if (data.includes(oldUtf8)) {
|
|
||||||
const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
|
||||||
if (count > 0) {
|
|
||||||
zip.updateFile(entry.entryName, patchedData);
|
|
||||||
totalCount += count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalCount > 0) {
|
|
||||||
zip.writeZip(serverPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressCallback) progressCallback('Complete', 100);
|
|
||||||
return { success: true, patchCount: totalCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the client binary path based on platform
|
* Find the client binary path based on platform
|
||||||
*/
|
*/
|
||||||
@@ -961,7 +608,6 @@ class ClientPatcher {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
findServerPath(gameDir) {
|
findServerPath(gameDir) {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(gameDir, 'Server', 'HytaleServer.jar'),
|
path.join(gameDir, 'Server', 'HytaleServer.jar'),
|
||||||
@@ -978,9 +624,6 @@ class ClientPatcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure both client and server are patched before launching
|
* Ensure both client and server are patched before launching
|
||||||
* @param {string} gameDir - Path to the game directory
|
|
||||||
* @param {function} progressCallback - Optional callback for progress updates
|
|
||||||
* @param {string} javaPath - Optional path to Java executable for server patching
|
|
||||||
*/
|
*/
|
||||||
async ensureClientPatched(gameDir, progressCallback, javaPath = null) {
|
async ensureClientPatched(gameDir, progressCallback, javaPath = null) {
|
||||||
const results = {
|
const results = {
|
||||||
@@ -991,13 +634,9 @@ class ClientPatcher {
|
|||||||
|
|
||||||
const clientPath = this.findClientPath(gameDir);
|
const clientPath = this.findClientPath(gameDir);
|
||||||
if (clientPath) {
|
if (clientPath) {
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching client binary...', 10);
|
||||||
progressCallback('Patching client binary...', 10);
|
|
||||||
}
|
|
||||||
results.client = await this.patchClient(clientPath, (msg, pct) => {
|
results.client = await this.patchClient(clientPath, (msg, pct) => {
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
|
||||||
progressCallback(`Client: ${msg}`, pct ? pct / 2 : null);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn('Could not find HytaleClient binary');
|
console.warn('Could not find HytaleClient binary');
|
||||||
@@ -1006,13 +645,9 @@ class ClientPatcher {
|
|||||||
|
|
||||||
const serverPath = this.findServerPath(gameDir);
|
const serverPath = this.findServerPath(gameDir);
|
||||||
if (serverPath) {
|
if (serverPath) {
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching server JAR...', 50);
|
||||||
progressCallback('Patching server JAR...', 50);
|
|
||||||
}
|
|
||||||
results.server = await this.patchServer(serverPath, (msg, pct) => {
|
results.server = await this.patchServer(serverPath, (msg, pct) => {
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
|
||||||
progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
|
|
||||||
}
|
|
||||||
}, javaPath);
|
}, javaPath);
|
||||||
} else {
|
} else {
|
||||||
console.warn('Could not find HytaleServer.jar');
|
console.warn('Could not find HytaleServer.jar');
|
||||||
@@ -1023,9 +658,7 @@ class ClientPatcher {
|
|||||||
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched);
|
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.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Patching complete', 100);
|
||||||
progressCallback('Patching complete', 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|||||||
201
docs/STEAMDECK_CRASH_INVESTIGATION.md
Normal file
201
docs/STEAMDECK_CRASH_INVESTIGATION.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
# Steam Deck / Ubuntu LTS Crash Investigation
|
||||||
|
|
||||||
|
## Problem Summary
|
||||||
|
|
||||||
|
The Hytale F2P launcher's client patcher causes crashes on Steam Deck and Ubuntu LTS with the error:
|
||||||
|
```
|
||||||
|
free(): invalid pointer
|
||||||
|
```
|
||||||
|
|
||||||
|
The crash occurs after successful authentication, specifically right after "Finished handling RequiredAssets".
|
||||||
|
|
||||||
|
**Affected Systems:**
|
||||||
|
- Steam Deck (glibc 2.41)
|
||||||
|
- Ubuntu LTS
|
||||||
|
|
||||||
|
**Working Systems:**
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
- Arch Linux
|
||||||
|
|
||||||
|
**Critical Finding:** The UNPATCHED original binary works fine on Steam Deck. The crash is caused by our patching.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## String Occurrences Found
|
||||||
|
|
||||||
|
### UTF-16LE Format (3 occurrences)
|
||||||
|
Found by `HYTALE_PATCH_MODE=utf16le`:
|
||||||
|
|
||||||
|
| Index | Offset | Before Context | After Context | Likely URL |
|
||||||
|
|-------|--------|----------------|---------------|------------|
|
||||||
|
| 0 | 0x1bc5ad7 | `try.` | `/2i3...` | `sentry.hytale.com/2...` |
|
||||||
|
| 1 | 0x1bc5b3f | `s://` | `/hel` | `https://hytale.com/help...` |
|
||||||
|
| 2 | 0x1bc5bc9 | `ore.` | `/?up` | `store.hytale.com/?up...` |
|
||||||
|
|
||||||
|
### Length-Prefixed Format (1 occurrence)
|
||||||
|
Found by default `length-prefixed` mode:
|
||||||
|
|
||||||
|
| Offset | Before | After | Notes |
|
||||||
|
|--------|--------|-------|-------|
|
||||||
|
| 0x1bc5d63 | `5933b8` | `89338807` | **Surrounded by what looks like x86 code!** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Finding: Binary Diff Analysis
|
||||||
|
|
||||||
|
When patching with length-prefixed mode (single occurrence):
|
||||||
|
|
||||||
|
```
|
||||||
|
< 01bc5d60: 5933 b80a 0000 0068 0079 0074 0061 006c Y3.....h.y.t.a.l
|
||||||
|
< 01bc5d70: 0065 002e 0063 006f 006d 8933 8807 0000 .e...c.o.m.3....
|
||||||
|
---
|
||||||
|
> 01bc5d60: 5933 b80a 0000 0073 0061 006e 0061 0073 Y3.....s.a.n.a.s
|
||||||
|
> 01bc5d70: 006f 006c 002e 0077 0073 8933 8807 0000 .o.l...w.s.3....
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure at 0x1bc5d60:**
|
||||||
|
```
|
||||||
|
5933 b8 | 0a000000 | 68007900740061006c0065002e0063006f006d | 8933 8807 0000
|
||||||
|
???????? | len=10 | h.y.t.a.l.e...c.o.m | mov [rbx],esi?
|
||||||
|
```
|
||||||
|
|
||||||
|
- `5933 b8` before the string - could be code or metadata
|
||||||
|
- `0a 00 00 00` - .NET length prefix (10 characters)
|
||||||
|
- String content in UTF-16LE
|
||||||
|
- `89 33` after - this is `mov [rbx], esi` in x86-64!
|
||||||
|
|
||||||
|
**The string appears to be embedded near executable code, not in a clean data section.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Results Summary
|
||||||
|
|
||||||
|
| Test | Occurrences Patched | Auth Works | Crashes |
|
||||||
|
|------|---------------------|------------|---------|
|
||||||
|
| Length-prefixed (default) | 1 at 0x1bc5d63 | YES | YES |
|
||||||
|
| UTF-16LE mode | 3 at 0x1bc5ad7, 0x1bc5b3f, 0x1bc5bc9 | YES | YES |
|
||||||
|
| Skip all UTF-16LE | 0 (but legacy fallback patched 4!) | YES | YES |
|
||||||
|
| Original unpatched | 0 | NO (wrong issuer) | NO |
|
||||||
|
|
||||||
|
**Key Insight:** Even patching just ONE string (the length-prefixed one) causes the crash, yet authentication succeeds before the crash.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GDB Stack Trace
|
||||||
|
|
||||||
|
```
|
||||||
|
#0 0x00007ffff7d3f5a4 in ?? () from /usr/lib/libc.so.6
|
||||||
|
#1 raise () from /usr/lib/libc.so.6
|
||||||
|
#2 abort () from /usr/lib/libc.so.6
|
||||||
|
#3-#4 ?? () from /usr/lib/libc.so.6
|
||||||
|
#5 free () from /usr/lib/libc.so.6
|
||||||
|
#6 ?? () from libzstd.so <-- CRASH POINT
|
||||||
|
#7-#24 HytaleClient code (asset decompression)
|
||||||
|
```
|
||||||
|
|
||||||
|
Crash occurs in `libzstd.so` during `free()` after "Finished handling RequiredAssets".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hypotheses
|
||||||
|
|
||||||
|
### 1. .NET String Interning
|
||||||
|
.NET AOT may have precomputed hashes or metadata for interned strings. Modifying the string content breaks the hash, causing memory corruption when the runtime tries to use it.
|
||||||
|
|
||||||
|
### 2. Code/Data Boundary Issue
|
||||||
|
The string at 0x1bc5d63 appears to be embedded near x86 code (`89 33` = `mov [rbx], esi`). Modifying it might corrupt instruction decoding or memory layout calculations.
|
||||||
|
|
||||||
|
### 3. Checksums/Integrity
|
||||||
|
The binary may have checksums for certain data sections that we're invalidating.
|
||||||
|
|
||||||
|
### 4. Memory Alignment
|
||||||
|
glibc 2.41's stricter heap validation may catch alignment issues that older versions ignore.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Example |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `HYTALE_AUTH_DOMAIN` | Target domain | `sanasol.ws` |
|
||||||
|
| `HYTALE_PATCH_MODE` | `utf16le` or `length-prefixed` | `utf16le` |
|
||||||
|
| `HYTALE_SKIP_SENTRY_PATCH` | Skip sentry URL patch | `1` |
|
||||||
|
| `HYTALE_SKIP_SUBDOMAIN_PATCH` | Skip subdomain patches | `1` |
|
||||||
|
| `HYTALE_PATCH_LIMIT` | Max patches to apply | `1` |
|
||||||
|
| `HYTALE_PATCH_SKIP` | Comma-separated indices to skip | `0,2` |
|
||||||
|
| `HYTALE_NO_LEGACY_FALLBACK` | Disable legacy fallback | `1` |
|
||||||
|
| `HYTALE_NOOP_TEST` | Read/write without patching | `1` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files & Offsets Reference
|
||||||
|
|
||||||
|
**Binary:** `HytaleClient` (ELF 64-bit, ~39.9 MB)
|
||||||
|
|
||||||
|
| Offset | Format | Content |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| 0x1bc5ad7 | UTF-16LE | `sentry.hytale.com/...` |
|
||||||
|
| 0x1bc5b3f | UTF-16LE | `https://hytale.com/help...` |
|
||||||
|
| 0x1bc5bc9 | UTF-16LE | `store.hytale.com/?...` |
|
||||||
|
| 0x1bc5d63 | Length-prefixed | Main session URL (surrounded by code?) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SOLUTION FOUND ✓
|
||||||
|
|
||||||
|
### Root Cause
|
||||||
|
The crash is caused by **glibc 2.41's stricter heap validation** catching a pre-existing race condition in the .NET AOT runtime or asset decompression code. Our binary patching triggers this timing-dependent bug, but the patching itself is correct.
|
||||||
|
|
||||||
|
### Evidence
|
||||||
|
- Valgrind showed NO memory corruption errors
|
||||||
|
- Game ran successfully under Valgrind (slower execution avoids the race)
|
||||||
|
- Game was manually killed (SIGINT), not crashed
|
||||||
|
- 1.4M allocations with no "Invalid free" detected
|
||||||
|
|
||||||
|
### Fix: Use jemalloc allocator
|
||||||
|
jemalloc handles the race condition gracefully. The launcher now auto-detects and uses jemalloc on Linux.
|
||||||
|
|
||||||
|
**Install jemalloc:**
|
||||||
|
```bash
|
||||||
|
# Steam Deck / Arch Linux
|
||||||
|
sudo pacman -S jemalloc
|
||||||
|
|
||||||
|
# Ubuntu / Debian
|
||||||
|
sudo apt install libjemalloc2
|
||||||
|
|
||||||
|
# Fedora / RHEL
|
||||||
|
sudo dnf install jemalloc
|
||||||
|
```
|
||||||
|
|
||||||
|
The launcher automatically uses jemalloc if found at:
|
||||||
|
- `/usr/lib/libjemalloc.so.2` (Arch, Steam Deck)
|
||||||
|
- `/usr/lib/x86_64-linux-gnu/libjemalloc.so.2` (Debian/Ubuntu)
|
||||||
|
- `/usr/lib64/libjemalloc.so.2` (Fedora/RHEL)
|
||||||
|
|
||||||
|
**Manual workaround (if launcher doesn't detect):**
|
||||||
|
```bash
|
||||||
|
LD_PRELOAD=/usr/lib/libjemalloc.so.2 ./Client/HytaleClient ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Disable jemalloc (for testing):**
|
||||||
|
```bash
|
||||||
|
HYTALE_NO_JEMALLOC=1 npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Previous Investigation (for reference)
|
||||||
|
|
||||||
|
### Next Steps (COMPLETED)
|
||||||
|
|
||||||
|
1. ~~Try runtime hooking instead of binary patching~~ - Not needed, jemalloc fixes the issue
|
||||||
|
2. ~~Investigate .NET AOT string metadata~~ - Not the root cause
|
||||||
|
3. ~~Test on different glibc versions~~ - Confirmed glibc 2.41 specific
|
||||||
|
4. ~~Examine libzstd interaction~~ - libzstd's free() was just where the corruption manifested
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Branch
|
||||||
|
|
||||||
|
`fix/patcher-memory-corruption-v2`
|
||||||
95
docs/STEAMDECK_DEBUG_COMMANDS.md
Normal file
95
docs/STEAMDECK_DEBUG_COMMANDS.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Steam Deck / Linux Crash Fix
|
||||||
|
|
||||||
|
## SOLUTION: Use jemalloc ✓
|
||||||
|
|
||||||
|
The crash is caused by glibc 2.41's stricter heap validation. Using jemalloc as the memory allocator fixes the issue.
|
||||||
|
|
||||||
|
### Install jemalloc
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Steam Deck / Arch Linux
|
||||||
|
sudo pacman -S jemalloc
|
||||||
|
|
||||||
|
# Ubuntu / Debian
|
||||||
|
sudo apt install libjemalloc2
|
||||||
|
|
||||||
|
# Fedora / RHEL
|
||||||
|
sudo dnf install jemalloc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Launcher Auto-Detection
|
||||||
|
|
||||||
|
The launcher automatically uses jemalloc when installed. No manual configuration needed.
|
||||||
|
|
||||||
|
To disable (for testing):
|
||||||
|
```bash
|
||||||
|
HYTALE_NO_JEMALLOC=1 npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Launch with jemalloc
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest
|
||||||
|
|
||||||
|
LD_PRELOAD=/usr/lib/libjemalloc.so.2 ./Client/HytaleClient --app-dir /home/deck/.hytalef2p/release/package/game/latest --java-exec /home/deck/.hytalef2p/release/package/jre/latest/bin/java --auth-mode authenticated --uuid YOUR_UUID --name Player --identity-token YOUR_TOKEN --session-token YOUR_TOKEN --user-dir /home/deck/.hytalesaves
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug Commands (for troubleshooting)
|
||||||
|
|
||||||
|
### Base Command
|
||||||
|
```bash
|
||||||
|
cd ~/.hytalef2p/release/package/game/latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### GDB Stack Trace (for crash analysis)
|
||||||
|
```bash
|
||||||
|
gdb -ex "run --app-dir ..." ./Client/HytaleClient
|
||||||
|
|
||||||
|
# After crash:
|
||||||
|
bt
|
||||||
|
bt full
|
||||||
|
info registers
|
||||||
|
quit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test glibc tunables (alternative fixes that didn't work reliably)
|
||||||
|
|
||||||
|
**Disable tcache:**
|
||||||
|
```bash
|
||||||
|
GLIBC_TUNABLES=glibc.malloc.tcache_count=0 ./Client/HytaleClient ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**Disable heap validation:**
|
||||||
|
```bash
|
||||||
|
MALLOC_CHECK_=0 ./Client/HytaleClient ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binary Validation
|
||||||
|
```bash
|
||||||
|
file ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
|
||||||
|
ldd ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hex Dump Commands
|
||||||
|
```bash
|
||||||
|
# Search for hytale.com UTF-16LE
|
||||||
|
xxd ~/.hytalef2p/release/package/game/latest/Client/HytaleClient.original | grep "6800 7900 7400 6100 6c00 6500 2e00 6300 6f00 6d00"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Different Patch Modes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restore original
|
||||||
|
cp ~/.hytalef2p/release/package/game/latest/Client/HytaleClient.original ~/.hytalef2p/release/package/game/latest/Client/HytaleClient
|
||||||
|
rm ~/.hytalef2p/release/package/game/latest/Client/HytaleClient.patched_custom
|
||||||
|
|
||||||
|
# Test UTF-16LE mode
|
||||||
|
HYTALE_PATCH_MODE=utf16le HYTALE_AUTH_DOMAIN=sanasol.ws npm start
|
||||||
|
|
||||||
|
# Test length-prefixed mode (default)
|
||||||
|
HYTALE_AUTH_DOMAIN=sanasol.ws npm start
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user