mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-26 06:41:47 -03:00
Collects launcher logs, game client logs, and config snapshot into a ZIP file and uploads to auth server. Shows submission ID for sharing with support. Includes i18n for all 11 locales.
239 lines
8.1 KiB
JavaScript
239 lines
8.1 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const zlib = require('zlib');
|
|
const logger = require('../logger');
|
|
|
|
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB per file
|
|
|
|
/**
|
|
* Get the HytaleSaves directory (game client logs)
|
|
*/
|
|
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');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read a log file, capping at MAX_FILE_SIZE (keeps tail/most recent lines)
|
|
*/
|
|
function readLogFile(filePath) {
|
|
try {
|
|
const stats = fs.statSync(filePath);
|
|
if (stats.size <= MAX_FILE_SIZE) {
|
|
return fs.readFileSync(filePath, 'utf8');
|
|
}
|
|
|
|
// Read only the last MAX_FILE_SIZE bytes
|
|
const fd = fs.openSync(filePath, 'r');
|
|
const buffer = Buffer.alloc(MAX_FILE_SIZE);
|
|
fs.readSync(fd, buffer, 0, MAX_FILE_SIZE, stats.size - MAX_FILE_SIZE);
|
|
fs.closeSync(fd);
|
|
|
|
const content = buffer.toString('utf8');
|
|
// Skip first partial line
|
|
const firstNewline = content.indexOf('\n');
|
|
const trimmed = firstNewline >= 0 ? content.substring(firstNewline + 1) : content;
|
|
return `[... truncated ${stats.size - MAX_FILE_SIZE} bytes ...]\n` + trimmed;
|
|
} catch (err) {
|
|
return `[Error reading file: ${err.message}]`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get files matching a date pattern from a directory
|
|
*/
|
|
function getFilesForDate(dir, dateStr, pattern) {
|
|
if (!fs.existsSync(dir)) return [];
|
|
|
|
try {
|
|
return fs.readdirSync(dir)
|
|
.filter(f => f.includes(dateStr) && (pattern ? pattern.test(f) : true))
|
|
.map(f => ({
|
|
name: f,
|
|
path: path.join(dir, f),
|
|
mtime: fs.statSync(path.join(dir, f)).mtime
|
|
}))
|
|
.sort((a, b) => b.mtime - a.mtime);
|
|
} catch (err) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// ZIP BUILDER (pure Node.js, no dependencies)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* CRC32 lookup table
|
|
*/
|
|
const crc32Table = new Uint32Array(256);
|
|
for (let i = 0; i < 256; i++) {
|
|
let c = i;
|
|
for (let j = 0; j < 8; j++) {
|
|
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
|
}
|
|
crc32Table[i] = c;
|
|
}
|
|
|
|
function crc32(buf) {
|
|
let crc = 0xFFFFFFFF;
|
|
for (let i = 0; i < buf.length; i++) {
|
|
crc = crc32Table[(crc ^ buf[i]) & 0xFF] ^ (crc >>> 8);
|
|
}
|
|
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
}
|
|
|
|
/**
|
|
* Create a ZIP file from an array of {name, content} entries
|
|
* Uses DEFLATE compression via built-in zlib
|
|
*/
|
|
function createZipBuffer(files) {
|
|
const localHeaders = [];
|
|
const centralEntries = [];
|
|
let offset = 0;
|
|
|
|
for (const file of files) {
|
|
const nameBytes = Buffer.from(file.name, 'utf8');
|
|
const contentBytes = Buffer.from(file.content, 'utf8');
|
|
const compressed = zlib.deflateRawSync(contentBytes);
|
|
const crcVal = crc32(contentBytes);
|
|
|
|
// Local file header (30 bytes + filename)
|
|
const local = Buffer.alloc(30);
|
|
local.writeUInt32LE(0x04034b50, 0); // signature
|
|
local.writeUInt16LE(20, 4); // version needed
|
|
local.writeUInt16LE(0, 6); // flags
|
|
local.writeUInt16LE(8, 8); // compression: DEFLATE
|
|
local.writeUInt16LE(0, 10); // mod time
|
|
local.writeUInt16LE(0, 12); // mod date
|
|
local.writeUInt32LE(crcVal, 14); // crc32
|
|
local.writeUInt32LE(compressed.length, 18); // compressed size
|
|
local.writeUInt32LE(contentBytes.length, 22); // uncompressed size
|
|
local.writeUInt16LE(nameBytes.length, 26); // filename length
|
|
local.writeUInt16LE(0, 28); // extra field length
|
|
|
|
localHeaders.push(local, nameBytes, compressed);
|
|
|
|
// Central directory entry (46 bytes + filename)
|
|
const central = Buffer.alloc(46);
|
|
central.writeUInt32LE(0x02014b50, 0); // signature
|
|
central.writeUInt16LE(20, 4); // version made by
|
|
central.writeUInt16LE(20, 6); // version needed
|
|
central.writeUInt16LE(0, 8); // flags
|
|
central.writeUInt16LE(8, 10); // compression
|
|
central.writeUInt16LE(0, 12); // mod time
|
|
central.writeUInt16LE(0, 14); // mod date
|
|
central.writeUInt32LE(crcVal, 16); // crc32
|
|
central.writeUInt32LE(compressed.length, 20); // compressed size
|
|
central.writeUInt32LE(contentBytes.length, 24); // uncompressed size
|
|
central.writeUInt16LE(nameBytes.length, 28); // filename length
|
|
central.writeUInt16LE(0, 30); // extra field length
|
|
central.writeUInt16LE(0, 32); // comment length
|
|
central.writeUInt16LE(0, 34); // disk number
|
|
central.writeUInt16LE(0, 36); // internal attrs
|
|
central.writeUInt32LE(0, 38); // external attrs
|
|
central.writeUInt32LE(offset, 42); // local header offset
|
|
|
|
centralEntries.push(central, nameBytes);
|
|
offset += 30 + nameBytes.length + compressed.length;
|
|
}
|
|
|
|
const centralDirBuf = Buffer.concat(centralEntries);
|
|
|
|
// End of central directory (22 bytes)
|
|
const eocd = Buffer.alloc(22);
|
|
eocd.writeUInt32LE(0x06054b50, 0); // signature
|
|
eocd.writeUInt16LE(0, 4); // disk number
|
|
eocd.writeUInt16LE(0, 6); // cd disk number
|
|
eocd.writeUInt16LE(files.length, 8); // entries on disk
|
|
eocd.writeUInt16LE(files.length, 10); // total entries
|
|
eocd.writeUInt32LE(centralDirBuf.length, 12); // cd size
|
|
eocd.writeUInt32LE(offset, 16); // cd offset
|
|
eocd.writeUInt16LE(0, 20); // comment length
|
|
|
|
return Buffer.concat([...localHeaders, centralDirBuf, eocd]);
|
|
}
|
|
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Collect all relevant logs for submission
|
|
* Returns { files: [{name, content}], meta: {username, platform, version} }
|
|
*/
|
|
function collectLogs() {
|
|
const files = [];
|
|
const today = new Date();
|
|
const todayStr = today.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
|
|
const yesterday = new Date(today);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
|
|
|
// 1. Launcher logs
|
|
const launcherLogDir = logger.getLogDirectory();
|
|
if (launcherLogDir && fs.existsSync(launcherLogDir)) {
|
|
// Today's launcher logs
|
|
const todayLogs = getFilesForDate(launcherLogDir, todayStr, /^launcher-.*\.log$/);
|
|
for (const f of todayLogs) {
|
|
files.push({ name: `launcher/${f.name}`, content: readLogFile(f.path) });
|
|
}
|
|
|
|
// Most recent from yesterday (just one)
|
|
const yesterdayLogs = getFilesForDate(launcherLogDir, yesterdayStr, /^launcher-.*\.log$/);
|
|
if (yesterdayLogs.length > 0) {
|
|
files.push({ name: `launcher/${yesterdayLogs[0].name}`, content: readLogFile(yesterdayLogs[0].path) });
|
|
}
|
|
}
|
|
|
|
// 2. Game client logs
|
|
const savesDir = getHytaleSavesDir();
|
|
const clientLogDir = path.join(savesDir, 'Logs');
|
|
if (fs.existsSync(clientLogDir)) {
|
|
const clientLogs = getFilesForDate(clientLogDir, todayStr, /client\.log$/);
|
|
for (const f of clientLogs) {
|
|
files.push({ name: `client/${f.name}`, content: readLogFile(f.path) });
|
|
}
|
|
}
|
|
|
|
// 3. Config snapshot
|
|
const appDir = logger.getAppDir ? logger.getAppDir() : logger.getInstallPath();
|
|
const configPath = path.join(appDir, 'config.json');
|
|
if (fs.existsSync(configPath)) {
|
|
try {
|
|
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
files.push({ name: 'config.json', content: configContent });
|
|
} catch (err) {
|
|
files.push({ name: 'config.json', content: `[Error reading config: ${err.message}]` });
|
|
}
|
|
}
|
|
|
|
// Build metadata
|
|
const { app } = require('electron');
|
|
let username = 'unknown';
|
|
try {
|
|
const configFile = path.join(appDir, 'config.json');
|
|
if (fs.existsSync(configFile)) {
|
|
const cfg = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
username = cfg.username || cfg.playerName || 'unknown';
|
|
}
|
|
} catch (err) {}
|
|
|
|
return {
|
|
files,
|
|
meta: {
|
|
username,
|
|
platform: `${process.platform}-${process.arch}`,
|
|
version: app.getVersion()
|
|
}
|
|
};
|
|
}
|
|
|
|
module.exports = { collectLogs, createZipBuffer };
|