mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-26 14:51:48 -03:00
Compare commits
1 Commits
fix/patche
...
v2.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae375f9b6e |
12
PKGBUILD
12
PKGBUILD
@@ -14,15 +14,15 @@ source=("$url/archive/v$pkgver.tar.gz" "Hytale-F2P.desktop")
|
||||
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
||||
|
||||
build() {
|
||||
cd "$pkgname-$pkgver"
|
||||
cd "$_pkgname-$pkgver"
|
||||
npm ci
|
||||
npm run build:arch
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$pkgname-$pkgver"
|
||||
install -d "$pkgdir/opt/$pkgname"
|
||||
cp -r dist/linux-unpacked/* "$pkgdir/opt/$pkgname"
|
||||
install -Dm644 "$srcdir/$pkgname.desktop" "$pkgdir/usr/share/applications/$pkgname.desktop"
|
||||
install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$pkgname.png"
|
||||
cd "$_pkgname-$pkgver"
|
||||
install -d "$pkgdir/opt/$_pkgname"
|
||||
cp -r dist/linux-unpacked/* "$pkgdir/opt/$_pkgname"
|
||||
install -Dm644 "$srcdir/$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
|
||||
install -Dm644 GUI/icon.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/$_pkgname.png"
|
||||
}
|
||||
|
||||
@@ -14,21 +14,6 @@ function getAppDir() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get centralized UserData saves directory (NEW in 2.1.2)
|
||||
* UserData is now stored separately from game installation
|
||||
*/
|
||||
function getHytaleSavesDir() {
|
||||
const home = os.homedir();
|
||||
if (process.platform === 'win32') {
|
||||
return path.join(home, 'AppData', 'Local', 'HytaleSaves');
|
||||
} else if (process.platform === 'darwin') {
|
||||
return path.join(home, 'Library', 'Application Support', 'HytaleSaves');
|
||||
} else {
|
||||
return path.join(home, '.hytalesaves');
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_APP_DIR = getAppDir();
|
||||
|
||||
function getResolvedAppDir(customPath) {
|
||||
@@ -233,8 +218,20 @@ async function getModsPath(customInstallPath = null) {
|
||||
|
||||
function getProfilesDir(customInstallPath = null) {
|
||||
try {
|
||||
// NEW 2.1.2: Use centralized UserData location
|
||||
const userDataPath = getHytaleSavesDir();
|
||||
// get UserData path
|
||||
let installPath = customInstallPath;
|
||||
if (!installPath) {
|
||||
const configFile = path.join(DEFAULT_APP_DIR, 'config.json');
|
||||
if (fs.existsSync(configFile)) {
|
||||
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
||||
installPath = config.installPath || '';
|
||||
}
|
||||
}
|
||||
if (!installPath) installPath = getAppDir();
|
||||
|
||||
const branch = loadVersionBranch();
|
||||
const gameLatest = path.join(installPath, branch, 'package', 'game', 'latest');
|
||||
const userDataPath = findUserDataPath(gameLatest);
|
||||
const profilesDir = path.join(userDataPath, 'Profiles');
|
||||
|
||||
if (!fs.existsSync(profilesDir)) {
|
||||
@@ -250,7 +247,6 @@ function getProfilesDir(customInstallPath = null) {
|
||||
|
||||
module.exports = {
|
||||
getAppDir,
|
||||
getHytaleSavesDir,
|
||||
getResolvedAppDir,
|
||||
expandHome,
|
||||
APP_DIR,
|
||||
|
||||
@@ -12,7 +12,6 @@ const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA
|
||||
const { getLatestClientVersion } = require('../services/versionManager');
|
||||
const { updateGameFiles } = require('./gameManager');
|
||||
const { syncModsForCurrentProfile } = require('./modManager');
|
||||
const { getUserDataPath } = require('../utils/userDataMigration');
|
||||
|
||||
// Client patcher for custom auth server (sanasol.ws)
|
||||
let clientPatcher = null;
|
||||
@@ -107,9 +106,7 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
||||
const customAppDir = getResolvedAppDir(installPathOverride);
|
||||
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
||||
|
||||
// NEW 2.1.2: Use centralized UserData location
|
||||
const userDataDir = getUserDataPath();
|
||||
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
|
||||
|
||||
const gameLatest = customGameDir;
|
||||
let clientPath = findClientPath(gameLatest);
|
||||
@@ -285,32 +282,6 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
||||
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 {
|
||||
let spawnOptions = {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
|
||||
@@ -9,7 +9,7 @@ const { installButler } = require('./butlerManager');
|
||||
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
||||
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config');
|
||||
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
|
||||
const { getUserDataPath, migrateUserDataToCentralized } = require('../utils/userDataMigration');
|
||||
const userDataBackup = require('../utils/userDataBackup');
|
||||
|
||||
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
||||
const osName = getOS();
|
||||
@@ -308,25 +308,31 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir
|
||||
|
||||
async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, cacheDir = CACHE_DIR, branchOverride = null) {
|
||||
let tempUpdateDir;
|
||||
let backupPath = null;
|
||||
const branch = branchOverride || loadVersionBranch();
|
||||
const installPath = path.dirname(path.dirname(path.dirname(path.dirname(gameDir))));
|
||||
|
||||
// Vérifier si on a version_client et version_branch dans config.json
|
||||
const config = loadConfig();
|
||||
const oldBranch = config.version_branch || 'release';
|
||||
const hasVersionConfig = !!(config.version_client && config.version_branch);
|
||||
const oldBranch = config.version_branch || 'release'; // L'ancienne branche pour le backup
|
||||
console.log(`[UpdateGameFiles] hasVersionConfig: ${hasVersionConfig}`);
|
||||
console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`);
|
||||
|
||||
try {
|
||||
// NEW 2.1.2: Ensure UserData migration to centralized location
|
||||
if (progressCallback) {
|
||||
progressCallback('Backing up user data...', 5, null, null, null);
|
||||
}
|
||||
|
||||
// Backup UserData AVANT de télécharger/installer (critical for same-branch updates)
|
||||
try {
|
||||
console.log('[UpdateGameFiles] Ensuring UserData migration...');
|
||||
const migrationResult = await migrateUserDataToCentralized();
|
||||
if (migrationResult.migrated) {
|
||||
console.log('[UpdateGameFiles] ✓ UserData migrated to centralized location');
|
||||
} else if (migrationResult.alreadyMigrated) {
|
||||
console.log('[UpdateGameFiles] ✓ UserData already in centralized location');
|
||||
console.log(`[UpdateGameFiles] Attempting to backup UserData from old branch: ${oldBranch}`);
|
||||
backupPath = await userDataBackup.backupUserData(installPath, oldBranch, hasVersionConfig);
|
||||
if (backupPath) {
|
||||
console.log(`[UpdateGameFiles] ✓ UserData backed up from ${oldBranch}: ${backupPath}`);
|
||||
}
|
||||
} catch (migrationError) {
|
||||
console.warn('[UpdateGameFiles] UserData migration warning:', migrationError.message);
|
||||
} catch (backupError) {
|
||||
console.warn('[UpdateGameFiles] ✗ UserData backup failed:', backupError.message);
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
@@ -384,9 +390,31 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
||||
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
|
||||
console.log('Logo@2x.png update result after update:', logoResult);
|
||||
|
||||
// NEW 2.1.2: No longer create UserData in game installation
|
||||
// UserData is now in centralized location (getUserDataPath())
|
||||
console.log('[UpdateGameFiles] UserData is managed in centralized location');
|
||||
// Ensure UserData directory exists
|
||||
const userDataDir = path.join(gameDir, 'Client', 'UserData');
|
||||
if (!fs.existsSync(userDataDir)) {
|
||||
console.log(`[UpdateGameFiles] Creating UserData directory at: ${userDataDir}`);
|
||||
fs.mkdirSync(userDataDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Restoring user data...', 90, null, null, null);
|
||||
}
|
||||
|
||||
// Restore UserData using new system
|
||||
if (backupPath) {
|
||||
try {
|
||||
console.log(`[UpdateGameFiles] Restoring UserData from ${oldBranch} to ${branch}`);
|
||||
console.log(`[UpdateGameFiles] Source backup: ${backupPath}`);
|
||||
await userDataBackup.restoreUserData(backupPath, installPath, branch);
|
||||
await userDataBackup.cleanupBackup(backupPath);
|
||||
console.log(`[UpdateGameFiles] ✓ UserData migrated successfully from ${oldBranch} to ${branch}`);
|
||||
} catch (restoreError) {
|
||||
console.warn('[UpdateGameFiles] ✗ UserData restore failed:', restoreError.message);
|
||||
}
|
||||
} else {
|
||||
console.log('[UpdateGameFiles] No backup to restore, empty UserData folder created');
|
||||
}
|
||||
|
||||
console.log(`Game files updated successfully to version: ${newVersion}`);
|
||||
|
||||
@@ -406,6 +434,15 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
||||
} catch (error) {
|
||||
console.error('Error updating game files:', error);
|
||||
|
||||
if (backupPath) {
|
||||
try {
|
||||
await userDataBackup.cleanupBackup(backupPath);
|
||||
console.log('UserData backup cleaned up after error');
|
||||
} catch (cleanupError) {
|
||||
console.warn('Could not clean up UserData backup:', cleanupError.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (tempUpdateDir && fs.existsSync(tempUpdateDir)) {
|
||||
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -433,18 +470,28 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
||||
const customToolsDir = path.join(customAppDir, 'butler');
|
||||
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
||||
const userDataDir = path.join(customGameDir, 'Client', 'UserData');
|
||||
|
||||
// Vérifier si on a version_client et version_branch dans config.json
|
||||
const config = loadConfig();
|
||||
const hasVersionConfig = !!(config.version_client && config.version_branch);
|
||||
console.log(`[InstallGame] Configuration detected - version_client: ${config.version_client}, version_branch: ${config.version_branch}`);
|
||||
console.log(`[InstallGame] hasVersionConfig: ${hasVersionConfig}`);
|
||||
|
||||
// Backup UserData AVANT l'installation si nécessaire
|
||||
let backupPath = null;
|
||||
if (progressCallback) {
|
||||
progressCallback('Checking for existing UserData...', 5, null, null, null);
|
||||
}
|
||||
|
||||
// NEW 2.1.2: Ensure UserData migration to centralized location
|
||||
try {
|
||||
console.log('[InstallGame] Ensuring UserData migration...');
|
||||
const migrationResult = await migrateUserDataToCentralized();
|
||||
if (migrationResult.migrated) {
|
||||
console.log('[InstallGame] ✓ UserData migrated to centralized location');
|
||||
} else if (migrationResult.alreadyMigrated) {
|
||||
console.log('[InstallGame] ✓ UserData already in centralized location');
|
||||
console.log(`[InstallGame] Attempting UserData backup (hasVersionConfig: ${hasVersionConfig})...`);
|
||||
backupPath = await userDataBackup.backupUserData(customAppDir, branch, hasVersionConfig);
|
||||
if (backupPath) {
|
||||
console.log(`[InstallGame] ✓ UserData backed up to: ${backupPath}`);
|
||||
}
|
||||
} catch (migrationError) {
|
||||
console.warn('[InstallGame] UserData migration warning:', migrationError.message);
|
||||
} catch (backupError) {
|
||||
console.warn('[InstallGame] ✗ UserData backup failed:', backupError.message);
|
||||
}
|
||||
|
||||
[customAppDir, customCacheDir, customToolsDir].forEach(dir => {
|
||||
@@ -453,6 +500,10 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
||||
}
|
||||
});
|
||||
|
||||
if (!fs.existsSync(userDataDir)) {
|
||||
fs.mkdirSync(userDataDir, { recursive: true });
|
||||
}
|
||||
|
||||
saveUsername(playerName);
|
||||
if (installPathOverride) {
|
||||
saveInstallPath(installPathOverride);
|
||||
@@ -544,9 +595,29 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
||||
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
|
||||
console.log('Logo@2x.png update result after installation:', logoResult);
|
||||
|
||||
// NEW 2.1.2: No longer create UserData in game installation
|
||||
// UserData is managed in centralized location (getUserDataPath())
|
||||
console.log('[InstallGame] UserData is managed in centralized location');
|
||||
// Ensure UserData directory exists
|
||||
if (!fs.existsSync(userDataDir)) {
|
||||
console.log(`[InstallGame] Creating UserData directory at: ${userDataDir}`);
|
||||
fs.mkdirSync(userDataDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Restore UserData from backup if exists
|
||||
if (backupPath) {
|
||||
if (progressCallback) {
|
||||
progressCallback('Restoring UserData...', 95, null, null, null);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[InstallGame] Restoring UserData from: ${backupPath}`);
|
||||
await userDataBackup.restoreUserData(backupPath, customAppDir, branch);
|
||||
await userDataBackup.cleanupBackup(backupPath);
|
||||
console.log('[InstallGame] ✓ UserData restored successfully');
|
||||
} catch (restoreError) {
|
||||
console.warn('[InstallGame] ✗ UserData restore failed:', restoreError.message);
|
||||
}
|
||||
} else {
|
||||
console.log('[InstallGame] No backup to restore, empty UserData folder created');
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Installation complete', 100, null, null, null);
|
||||
|
||||
@@ -147,9 +147,8 @@ class ClientPatcher {
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace bytes in buffer with null-padding for shorter replacements
|
||||
* 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
|
||||
* Replace bytes in buffer - only overwrites the length of new bytes
|
||||
* Prevents offset corruption by not expanding the replacement
|
||||
*/
|
||||
replaceBytes(buffer, oldBytes, newBytes) {
|
||||
let count = 0;
|
||||
@@ -163,20 +162,7 @@ class ClientPatcher {
|
||||
const positions = this.findAllOccurrences(result, oldBytes);
|
||||
|
||||
for (const pos of positions) {
|
||||
// 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
|
||||
// Only overwrite the length of the new bytes
|
||||
newBytes.copy(result, pos);
|
||||
count++;
|
||||
}
|
||||
@@ -184,65 +170,6 @@ class ClientPatcher {
|
||||
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.
|
||||
* Java stores strings in UTF-8 format in the constant pool.
|
||||
@@ -270,19 +197,11 @@ class ClientPatcher {
|
||||
* .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
|
||||
*
|
||||
* IMPORTANT: newDomain must be same length or shorter than oldDomain to avoid buffer overflow
|
||||
*/
|
||||
findAndReplaceDomainSmart(data, oldDomain, newDomain) {
|
||||
let count = 0;
|
||||
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 newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1));
|
||||
|
||||
@@ -298,11 +217,6 @@ class ClientPatcher {
|
||||
const lastCharFirstByte = result[lastCharPos];
|
||||
|
||||
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);
|
||||
|
||||
result[lastCharPos] = newLastCharByte;
|
||||
@@ -325,10 +239,6 @@ class ClientPatcher {
|
||||
/**
|
||||
* Apply all domain patches using length-prefixed format
|
||||
* 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://') {
|
||||
let result = Buffer.from(data);
|
||||
@@ -337,79 +247,50 @@ class ClientPatcher {
|
||||
|
||||
console.log(` Patching strategy: ${strategy.description}`);
|
||||
|
||||
// 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 newSentry = `${protocol}t@${domain}/2`;
|
||||
// 1. Patch telemetry/sentry URL
|
||||
const oldSentry = 'https://ca900df42fcf57d4dd8401a86ddd7da2@sentry.hytale.com/2';
|
||||
const newSentry = `${protocol}t@${domain}/2`;
|
||||
|
||||
console.log(` Patching sentry: ${oldSentry.slice(0, 30)}... -> ${newSentry}`);
|
||||
const sentryResult = this.replaceBytes(
|
||||
result,
|
||||
this.stringToLengthPrefixed(oldSentry),
|
||||
this.stringToLengthPrefixed(newSentry)
|
||||
);
|
||||
result = sentryResult.buffer;
|
||||
if (sentryResult.count > 0) {
|
||||
console.log(` Replaced ${sentryResult.count} sentry occurrence(s)`);
|
||||
totalCount += sentryResult.count;
|
||||
}
|
||||
console.log(` Patching sentry: ${oldSentry.slice(0, 30)}... -> ${newSentry}`);
|
||||
const sentryResult = this.replaceBytes(
|
||||
result,
|
||||
this.stringToLengthPrefixed(oldSentry),
|
||||
this.stringToLengthPrefixed(newSentry)
|
||||
);
|
||||
result = sentryResult.buffer;
|
||||
if (sentryResult.count > 0) {
|
||||
console.log(` Replaced ${sentryResult.count} sentry occurrence(s)`);
|
||||
totalCount += sentryResult.count;
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
|
||||
// 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,
|
||||
this.stringToLengthPrefixed(ORIGINAL_DOMAIN),
|
||||
this.stringToLengthPrefixed(strategy.mainDomain)
|
||||
);
|
||||
}
|
||||
|
||||
const domainResult = this.replaceBytes(
|
||||
result,
|
||||
this.stringToLengthPrefixed(ORIGINAL_DOMAIN),
|
||||
this.stringToLengthPrefixed(strategy.mainDomain)
|
||||
);
|
||||
result = domainResult.buffer;
|
||||
if (domainResult.count > 0) {
|
||||
console.log(` Replaced ${domainResult.count} domain occurrence(s)`);
|
||||
totalCount += domainResult.count;
|
||||
}
|
||||
|
||||
// 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 newSubdomainPrefix = protocol + strategy.subdomainPrefix;
|
||||
// 3. Patch subdomain prefixes
|
||||
const subdomains = ['https://tools.', 'https://sessions.', 'https://account-data.', 'https://telemetry.'];
|
||||
const newSubdomainPrefix = protocol + strategy.subdomainPrefix;
|
||||
|
||||
for (const sub of subdomains) {
|
||||
console.log(` Patching subdomain: ${sub} -> ${newSubdomainPrefix}`);
|
||||
const subResult = this.replaceBytes(
|
||||
result,
|
||||
this.stringToLengthPrefixed(sub),
|
||||
this.stringToLengthPrefixed(newSubdomainPrefix)
|
||||
);
|
||||
result = subResult.buffer;
|
||||
if (subResult.count > 0) {
|
||||
console.log(` Replaced ${subResult.count} occurrence(s)`);
|
||||
totalCount += subResult.count;
|
||||
}
|
||||
for (const sub of subdomains) {
|
||||
console.log(` Patching subdomain: ${sub} -> ${newSubdomainPrefix}`);
|
||||
const subResult = this.replaceBytes(
|
||||
result,
|
||||
this.stringToLengthPrefixed(sub),
|
||||
this.stringToLengthPrefixed(newSubdomainPrefix)
|
||||
);
|
||||
result = subResult.buffer;
|
||||
if (subResult.count > 0) {
|
||||
console.log(` Replaced ${subResult.count} occurrence(s)`);
|
||||
totalCount += subResult.count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,13 +298,38 @@ class ClientPatcher {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Patch Discord invite URLs from .gg/hytale to .gg/MHkEjepMQ7
|
||||
*/
|
||||
patchDiscordUrl(data) {
|
||||
// Disabled - no practical effect and was causing memory corruption
|
||||
return { buffer: Buffer.from(data), count: 0 };
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -577,14 +483,6 @@ class ClientPatcher {
|
||||
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)...');
|
||||
const { buffer: patchedData, count } = this.applyDomainPatches(data, newDomain);
|
||||
|
||||
@@ -592,17 +490,6 @@ class ClientPatcher {
|
||||
const { buffer: finalData, count: discordCount } = this.patchDiscordUrl(patchedData);
|
||||
|
||||
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...');
|
||||
|
||||
// Fallback to legacy patching for older binary formats
|
||||
|
||||
@@ -46,8 +46,7 @@ class UserDataBackup {
|
||||
console.log(`[UserDataBackup] Copying from ${userDataPath} to ${backupPath}...`);
|
||||
await fs.copy(userDataPath, backupPath, {
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||
errorOnExist: false
|
||||
});
|
||||
console.log('[UserDataBackup] ✓ Backup completed successfully');
|
||||
return backupPath;
|
||||
@@ -83,8 +82,7 @@ class UserDataBackup {
|
||||
|
||||
await fs.copy(backupPath, userDataPath, {
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||
errorOnExist: false
|
||||
});
|
||||
|
||||
console.log('UserData restore completed successfully');
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { getHytaleSavesDir, getResolvedAppDir } = require('../core/paths');
|
||||
const { loadConfig, saveConfig } = require('../core/config');
|
||||
|
||||
/**
|
||||
* NEW SYSTEM (2.1.2+): UserData Migration to Centralized Location
|
||||
*
|
||||
* UserData is now stored in a centralized location instead of inside game installation:
|
||||
* - Windows: %LOCALAPPDATA%\HytaleSaves\
|
||||
* - macOS: ~/Library/Application Support/HytaleSaves/
|
||||
* - Linux: ~/.hytalesaves/
|
||||
*
|
||||
* This eliminates the need for backup/restore during updates.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if migration to centralized UserData has been completed
|
||||
*/
|
||||
function isMigrationCompleted() {
|
||||
const config = loadConfig();
|
||||
return config.userDataMigrated === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark migration as completed
|
||||
*/
|
||||
function markMigrationCompleted() {
|
||||
saveConfig({ userDataMigrated: true });
|
||||
console.log('[UserDataMigration] Migration marked as completed in config');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find old UserData location (pre-2.1.2)
|
||||
* Searches in: installPath/branch/package/game/latest/Client/UserData
|
||||
*/
|
||||
function findOldUserDataPath() {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const installPath = getResolvedAppDir();
|
||||
const branch = config.version_branch || 'release';
|
||||
|
||||
console.log(`[UserDataMigration] Looking for old UserData...`);
|
||||
console.log(`[UserDataMigration] Install path: ${installPath}`);
|
||||
console.log(`[UserDataMigration] Branch: ${branch}`);
|
||||
|
||||
// Old location
|
||||
const oldPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData');
|
||||
console.log(`[UserDataMigration] Checking: ${oldPath}`);
|
||||
console.log(`[UserDataMigration] Checking: ${oldPath}`);
|
||||
|
||||
if (fs.existsSync(oldPath)) {
|
||||
console.log(`[UserDataMigration] ✓ Found old UserData at: ${oldPath}`);
|
||||
return oldPath;
|
||||
}
|
||||
|
||||
console.log(`[UserDataMigration] ✗ Not found at current branch location`);
|
||||
|
||||
// Try other branch if current doesn't exist
|
||||
const otherBranch = branch === 'release' ? 'pre-release' : 'release';
|
||||
const otherPath = path.join(installPath, otherBranch, 'package', 'game', 'latest', 'Client', 'UserData');
|
||||
console.log(`[UserDataMigration] Checking other branch: ${otherPath}`);
|
||||
console.log(`[UserDataMigration] Checking other branch: ${otherPath}`);
|
||||
|
||||
if (fs.existsSync(otherPath)) {
|
||||
console.log(`[UserDataMigration] ✓ Found old UserData in other branch at: ${otherPath}`);
|
||||
return otherPath;
|
||||
}
|
||||
|
||||
console.log('[UserDataMigration] ✗ No old UserData found in any branch');
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[UserDataMigration] Error finding old UserData:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate UserData from old location to new centralized location
|
||||
* One-time operation when upgrading to 2.1.2
|
||||
*/
|
||||
async function migrateUserDataToCentralized() {
|
||||
// Check if already migrated
|
||||
if (isMigrationCompleted()) {
|
||||
console.log('[UserDataMigration] Migration already completed, skipping');
|
||||
return { success: true, alreadyMigrated: true };
|
||||
}
|
||||
|
||||
console.log('[UserDataMigration] === Starting UserData Migration to Centralized Location ===');
|
||||
|
||||
const newUserDataPath = getHytaleSavesDir();
|
||||
console.log(`[UserDataMigration] Target location: ${newUserDataPath}`);
|
||||
|
||||
// Ensure new directory exists
|
||||
if (!fs.existsSync(newUserDataPath)) {
|
||||
fs.mkdirSync(newUserDataPath, { recursive: true });
|
||||
console.log('[UserDataMigration] Created new HytaleSaves directory');
|
||||
}
|
||||
|
||||
// Find old UserData
|
||||
const oldUserDataPath = findOldUserDataPath();
|
||||
|
||||
if (!oldUserDataPath) {
|
||||
console.log('[UserDataMigration] No old UserData found - fresh install or already migrated');
|
||||
// Don't mark as migrated - let it check again next time in case game gets installed later
|
||||
return { success: true, freshInstall: true };
|
||||
}
|
||||
|
||||
// Check if new location already has data (shouldn't happen, but safety check)
|
||||
const existingFiles = fs.readdirSync(newUserDataPath);
|
||||
if (existingFiles.length > 0) {
|
||||
console.warn('[UserDataMigration] New location already contains files, marking as migrated to avoid re-attempts');
|
||||
markMigrationCompleted();
|
||||
return { success: true, skipped: true, reason: 'target_not_empty' };
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[UserDataMigration] Copying from ${oldUserDataPath} to ${newUserDataPath}...`);
|
||||
|
||||
// Copy all UserData to new location
|
||||
await fs.copy(oldUserDataPath, newUserDataPath, {
|
||||
overwrite: false,
|
||||
errorOnExist: false,
|
||||
dereference: true // Follow symlinks to avoid EPERM errors on Windows
|
||||
});
|
||||
|
||||
console.log('[UserDataMigration] ✓ UserData copied successfully');
|
||||
|
||||
// Mark migration as completed
|
||||
markMigrationCompleted();
|
||||
|
||||
console.log('[UserDataMigration] === Migration Completed Successfully ===');
|
||||
return {
|
||||
success: true,
|
||||
migrated: true,
|
||||
from: oldUserDataPath,
|
||||
to: newUserDataPath
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('[UserDataMigration] ✗ Migration failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
from: oldUserDataPath,
|
||||
to: newUserDataPath
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the centralized UserData path (always use this in 2.1.2+)
|
||||
* Ensures directory exists
|
||||
*/
|
||||
function getUserDataPath() {
|
||||
const userDataPath = getHytaleSavesDir();
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(userDataPath)) {
|
||||
fs.mkdirSync(userDataPath, { recursive: true });
|
||||
console.log(`[UserDataMigration] Created UserData directory: ${userDataPath}`);
|
||||
}
|
||||
|
||||
return userDataPath;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
migrateUserDataToCentralized,
|
||||
getUserDataPath,
|
||||
isMigrationCompleted,
|
||||
findOldUserDataPath
|
||||
};
|
||||
9
main.js
9
main.js
@@ -5,7 +5,6 @@ const { autoUpdater } = require('electron-updater');
|
||||
const fs = require('fs');
|
||||
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher');
|
||||
const { retryPWRDownload } = require('./backend/managers/gameManager');
|
||||
const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration');
|
||||
|
||||
// Handle Hardware Acceleration
|
||||
try {
|
||||
@@ -299,14 +298,6 @@ app.whenReady().then(async () => {
|
||||
// Initialize Profile Manager (runs migration if needed)
|
||||
profileManager.init();
|
||||
|
||||
// Migrate UserData to centralized location (v2.1.2+)
|
||||
console.log('[Startup] Checking UserData migration...');
|
||||
try {
|
||||
await migrateUserDataToCentralized();
|
||||
} catch (error) {
|
||||
console.error('[Startup] UserData migration failed:', error);
|
||||
}
|
||||
|
||||
createSplashScreen();
|
||||
|
||||
setTimeout(async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hytale-f2p-launcher",
|
||||
"version": "2.1.2",
|
||||
"version": "2.1.1",
|
||||
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
||||
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
||||
"main": "main.js",
|
||||
|
||||
Reference in New Issue
Block a user