mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 11:41:49 -03:00
Compare commits
11 Commits
v2.3.5
...
fix/patche
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab6f932245 | ||
|
|
6333263ef9 | ||
|
|
654deca933 | ||
|
|
aab67e8e28 | ||
|
|
3953827f4a | ||
|
|
81d1e7c113 | ||
|
|
d8f90bd1ff | ||
|
|
50491abc69 | ||
|
|
c92c5bec3c | ||
|
|
73f67f2fec | ||
|
|
2582d9b6d1 |
@@ -285,6 +285,32 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
||||||
Object.assign(env, gpuEnv);
|
Object.assign(env, gpuEnv);
|
||||||
|
|
||||||
|
// Linux memory allocator fixes for "free(): invalid pointer" crashes
|
||||||
|
// on Steam Deck (glibc 2.41) and Ubuntu LTS
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
// Option 1: Disable glibc heap validation
|
||||||
|
env.MALLOC_CHECK_ = '0';
|
||||||
|
|
||||||
|
// Option 2: Try to use jemalloc if available (more robust allocator)
|
||||||
|
// User can set HYTALE_USE_JEMALLOC=1 to enable
|
||||||
|
if (process.env.HYTALE_USE_JEMALLOC === '1') {
|
||||||
|
const jemalloc = require('fs').existsSync('/usr/lib/libjemalloc.so.2')
|
||||||
|
? '/usr/lib/libjemalloc.so.2'
|
||||||
|
: require('fs').existsSync('/usr/lib/x86_64-linux-gnu/libjemalloc.so.2')
|
||||||
|
? '/usr/lib/x86_64-linux-gnu/libjemalloc.so.2'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (jemalloc) {
|
||||||
|
env.LD_PRELOAD = jemalloc + (env.LD_PRELOAD ? ':' + env.LD_PRELOAD : '');
|
||||||
|
console.log(`Linux: Using jemalloc allocator (${jemalloc})`);
|
||||||
|
} else {
|
||||||
|
console.log('Linux: jemalloc not found, using glibc with MALLOC_CHECK_=0');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Linux: Using glibc with MALLOC_CHECK_=0 (set HYTALE_USE_JEMALLOC=1 to try jemalloc)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let spawnOptions = {
|
let spawnOptions = {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
|||||||
@@ -147,8 +147,9 @@ 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
|
||||||
|
* that can cause memory corruption (free(): invalid pointer) on some systems
|
||||||
*/
|
*/
|
||||||
replaceBytes(buffer, oldBytes, newBytes) {
|
replaceBytes(buffer, oldBytes, newBytes) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -162,7 +163,20 @@ 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
|
// Log offset and surrounding bytes for debugging
|
||||||
|
const before = result.slice(Math.max(0, pos - 8), pos);
|
||||||
|
const after = result.slice(pos + oldBytes.length, Math.min(result.length, pos + oldBytes.length + 8));
|
||||||
|
console.log(` Patching at offset 0x${pos.toString(16)} (${pos})`);
|
||||||
|
console.log(` Before: ${before.toString('hex')}`);
|
||||||
|
console.log(` Old pattern: ${oldBytes.slice(0, 20).toString('hex')}${oldBytes.length > 20 ? '...' : ''}`);
|
||||||
|
console.log(` After: ${after.toString('hex')}`);
|
||||||
|
|
||||||
|
// First fill the entire old pattern region with zeros
|
||||||
|
// This prevents leftover bytes from causing memory corruption
|
||||||
|
if (newBytes.length < oldBytes.length) {
|
||||||
|
result.fill(0x00, pos, pos + oldBytes.length);
|
||||||
|
}
|
||||||
|
// Then write the new bytes
|
||||||
newBytes.copy(result, pos);
|
newBytes.copy(result, pos);
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
@@ -170,6 +184,65 @@ class ClientPatcher {
|
|||||||
return { buffer: result, count };
|
return { buffer: result, count };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace bytes with skip/limit control (for debugging)
|
||||||
|
* HYTALE_PATCH_SKIP: comma-separated indices to skip (e.g., "0,2" skips 1st and 3rd)
|
||||||
|
* HYTALE_PATCH_LIMIT: max number of patches to apply
|
||||||
|
*/
|
||||||
|
replaceBytesLimited(buffer, oldBytes, newBytes, limit) {
|
||||||
|
let count = 0;
|
||||||
|
const result = Buffer.from(buffer);
|
||||||
|
|
||||||
|
if (newBytes.length > oldBytes.length) {
|
||||||
|
console.warn(` Warning: New pattern (${newBytes.length}) longer than old (${oldBytes.length}), skipping`);
|
||||||
|
return { buffer: result, count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse skip list from env
|
||||||
|
const skipIndices = (process.env.HYTALE_PATCH_SKIP || '')
|
||||||
|
.split(',')
|
||||||
|
.filter(s => s.trim())
|
||||||
|
.map(s => parseInt(s.trim(), 10));
|
||||||
|
if (skipIndices.length > 0) {
|
||||||
|
console.log(` Skip indices: ${skipIndices.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = this.findAllOccurrences(result, oldBytes);
|
||||||
|
|
||||||
|
let patchedCount = 0;
|
||||||
|
for (let i = 0; i < positions.length; i++) {
|
||||||
|
const pos = positions[i];
|
||||||
|
|
||||||
|
// Log offset and surrounding bytes for debugging
|
||||||
|
const before = result.slice(Math.max(0, pos - 8), pos);
|
||||||
|
const after = result.slice(pos + oldBytes.length, Math.min(result.length, pos + oldBytes.length + 8));
|
||||||
|
|
||||||
|
if (skipIndices.includes(i)) {
|
||||||
|
console.log(` [${i}] Skipping offset 0x${pos.toString(16)} (in skip list)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patchedCount >= limit) {
|
||||||
|
console.log(` [${i}] Skipping offset 0x${pos.toString(16)} (limit reached)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` [${i}] Patching at offset 0x${pos.toString(16)} (${pos})`);
|
||||||
|
console.log(` Before: ${before.toString('hex')}`);
|
||||||
|
console.log(` Old pattern: ${oldBytes.slice(0, 20).toString('hex')}${oldBytes.length > 20 ? '...' : ''}`);
|
||||||
|
console.log(` After: ${after.toString('hex')}`);
|
||||||
|
|
||||||
|
if (newBytes.length < oldBytes.length) {
|
||||||
|
result.fill(0x00, pos, pos + oldBytes.length);
|
||||||
|
}
|
||||||
|
newBytes.copy(result, pos);
|
||||||
|
patchedCount++;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { buffer: result, count };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* Java stores strings in UTF-8 format in the constant pool.
|
||||||
@@ -197,11 +270,19 @@ class ClientPatcher {
|
|||||||
* .NET AOT stores some strings in various formats:
|
* .NET AOT stores some strings in various formats:
|
||||||
* - Standard UTF-16LE (each char is 2 bytes with \x00 high byte)
|
* - Standard UTF-16LE (each char is 2 bytes with \x00 high byte)
|
||||||
* - Length-prefixed where last char may have metadata byte instead of \x00
|
* - Length-prefixed where last char may have metadata byte instead of \x00
|
||||||
|
*
|
||||||
|
* IMPORTANT: newDomain must be same length or shorter than oldDomain to avoid buffer overflow
|
||||||
*/
|
*/
|
||||||
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
|
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const result = Buffer.from(data);
|
const result = Buffer.from(data);
|
||||||
|
|
||||||
|
// Safety check: new domain must not be longer than old
|
||||||
|
if (newDomain.length > oldDomain.length) {
|
||||||
|
console.warn(` Warning: New domain (${newDomain.length} chars) longer than old (${oldDomain.length} chars), 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,6 +298,11 @@ class ClientPatcher {
|
|||||||
const lastCharFirstByte = result[lastCharPos];
|
const lastCharFirstByte = result[lastCharPos];
|
||||||
|
|
||||||
if (lastCharFirstByte === oldLastCharByte) {
|
if (lastCharFirstByte === oldLastCharByte) {
|
||||||
|
// Zero-fill the old region first if new is shorter
|
||||||
|
if (newUtf16NoLast.length < oldUtf16NoLast.length) {
|
||||||
|
result.fill(0x00, pos, pos + oldUtf16NoLast.length);
|
||||||
|
}
|
||||||
|
|
||||||
newUtf16NoLast.copy(result, pos);
|
newUtf16NoLast.copy(result, pos);
|
||||||
|
|
||||||
result[lastCharPos] = newLastCharByte;
|
result[lastCharPos] = newLastCharByte;
|
||||||
@@ -239,6 +325,10 @@ 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
|
* This is the main patching method for variable-length domains
|
||||||
|
*
|
||||||
|
* Debug env vars:
|
||||||
|
* HYTALE_SKIP_SENTRY_PATCH=1 - Skip sentry URL patch (biggest size change)
|
||||||
|
* HYTALE_SKIP_SUBDOMAIN_PATCH=1 - Skip subdomain prefix patches
|
||||||
*/
|
*/
|
||||||
applyDomainPatches(data, domain, protocol = 'https://') {
|
applyDomainPatches(data, domain, protocol = 'https://') {
|
||||||
let result = Buffer.from(data);
|
let result = Buffer.from(data);
|
||||||
@@ -247,7 +337,10 @@ class ClientPatcher {
|
|||||||
|
|
||||||
console.log(` Patching strategy: ${strategy.description}`);
|
console.log(` Patching strategy: ${strategy.description}`);
|
||||||
|
|
||||||
// 1. Patch telemetry/sentry URL
|
// 1. Patch telemetry/sentry URL (skip if debugging)
|
||||||
|
if (process.env.HYTALE_SKIP_SENTRY_PATCH === '1') {
|
||||||
|
console.log(` Skipping sentry patch (HYTALE_SKIP_SENTRY_PATCH=1)`);
|
||||||
|
} else {
|
||||||
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`;
|
||||||
|
|
||||||
@@ -262,21 +355,46 @@ class ClientPatcher {
|
|||||||
console.log(` Replaced ${sentryResult.count} sentry occurrence(s)`);
|
console.log(` Replaced ${sentryResult.count} sentry occurrence(s)`);
|
||||||
totalCount += sentryResult.count;
|
totalCount += sentryResult.count;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Patch main domain (hytale.com -> mainDomain)
|
// 2. Patch main domain (hytale.com -> mainDomain)
|
||||||
|
// Try length-prefixed format first, then fall back to pure UTF-16LE
|
||||||
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
|
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
|
||||||
const domainResult = this.replaceBytes(
|
|
||||||
|
// Check for HYTALE_PATCH_MODE env var to test different formats
|
||||||
|
const patchMode = process.env.HYTALE_PATCH_MODE || 'length-prefixed';
|
||||||
|
console.log(` Patch mode: ${patchMode}`);
|
||||||
|
|
||||||
|
let domainResult;
|
||||||
|
if (patchMode === 'utf16le') {
|
||||||
|
// Pure UTF-16LE replacement (no length prefix)
|
||||||
|
const oldUtf16 = this.stringToUtf16LE(ORIGINAL_DOMAIN);
|
||||||
|
const newUtf16 = this.stringToUtf16LE(strategy.mainDomain);
|
||||||
|
console.log(` UTF-16LE: old=${oldUtf16.length} bytes, new=${newUtf16.length} bytes`);
|
||||||
|
|
||||||
|
// HYTALE_PATCH_LIMIT: only patch first N occurrences (for debugging)
|
||||||
|
const patchLimit = parseInt(process.env.HYTALE_PATCH_LIMIT || '999', 10);
|
||||||
|
console.log(` Patch limit: ${patchLimit}`);
|
||||||
|
domainResult = this.replaceBytesLimited(result, oldUtf16, newUtf16, patchLimit);
|
||||||
|
} else {
|
||||||
|
// Length-prefixed format (default)
|
||||||
|
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(` Replaced ${domainResult.count} domain occurrence(s)`);
|
||||||
totalCount += domainResult.count;
|
totalCount += domainResult.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Patch subdomain prefixes
|
// 3. Patch subdomain prefixes (skip if debugging)
|
||||||
|
if (process.env.HYTALE_SKIP_SUBDOMAIN_PATCH === '1') {
|
||||||
|
console.log(` Skipping subdomain patches (HYTALE_SKIP_SUBDOMAIN_PATCH=1)`);
|
||||||
|
} else {
|
||||||
const subdomains = ['https://tools.', 'https://sessions.', 'https://account-data.', 'https://telemetry.'];
|
const subdomains = ['https://tools.', 'https://sessions.', 'https://account-data.', 'https://telemetry.'];
|
||||||
const newSubdomainPrefix = protocol + strategy.subdomainPrefix;
|
const newSubdomainPrefix = protocol + strategy.subdomainPrefix;
|
||||||
|
|
||||||
@@ -293,43 +411,19 @@ class ClientPatcher {
|
|||||||
totalCount += subResult.count;
|
totalCount += subResult.count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { buffer: result, count: totalCount };
|
return { buffer: result, count: totalCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7
|
* Patch Discord invite URLs - DISABLED
|
||||||
|
* Was causing buffer overflow crashes on Steam Deck/Ubuntu LTS
|
||||||
|
* The Discord URL in the game doesn't affect F2P functionality anyway
|
||||||
*/
|
*/
|
||||||
patchDiscordUrl(data) {
|
patchDiscordUrl(data) {
|
||||||
let count = 0;
|
// Disabled - no practical effect and was causing memory corruption
|
||||||
const result = Buffer.from(data);
|
return { buffer: Buffer.from(data), count: 0 };
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -483,6 +577,14 @@ class ClientPatcher {
|
|||||||
progressCallback('Patching domain references...', 50);
|
progressCallback('Patching domain references...', 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HYTALE_NOOP_TEST: Just read and write binary without any changes
|
||||||
|
if (process.env.HYTALE_NOOP_TEST === '1') {
|
||||||
|
console.log('NOOP TEST: Writing binary without modifications...');
|
||||||
|
fs.writeFileSync(clientPath, data);
|
||||||
|
this.markAsPatched(clientPath);
|
||||||
|
return { success: true, patchCount: 0, noop: true };
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Applying domain patches (length-prefixed format)...');
|
console.log('Applying domain patches (length-prefixed format)...');
|
||||||
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
||||||
|
|
||||||
@@ -490,6 +592,17 @@ class ClientPatcher {
|
|||||||
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
|
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
|
||||||
|
|
||||||
if (count === 0 && discordCount === 0) {
|
if (count === 0 && discordCount === 0) {
|
||||||
|
// Check if we're in debug mode with skip - don't fallback if intentionally skipping
|
||||||
|
const hasSkipList = (process.env.HYTALE_PATCH_SKIP || '').trim().length > 0;
|
||||||
|
const noLegacyFallback = process.env.HYTALE_NO_LEGACY_FALLBACK === '1';
|
||||||
|
|
||||||
|
if (hasSkipList || noLegacyFallback) {
|
||||||
|
console.log('No occurrences patched (skip list active or legacy fallback disabled)');
|
||||||
|
fs.writeFileSync(clientPath, patchedData);
|
||||||
|
this.markAsPatched(clientPath);
|
||||||
|
return { success: true, patchCount: 0, skipped: true };
|
||||||
|
}
|
||||||
|
|
||||||
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
|
// Fallback to legacy patching for older binary formats
|
||||||
|
|||||||
Reference in New Issue
Block a user