mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-26 14:51:48 -03:00
Compare commits
6 Commits
552ec42d6c
...
v2.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19c8991a44 | ||
|
|
320ca54758 | ||
|
|
e14d56ef48 | ||
|
|
a649bf1fcc | ||
|
|
66faa1bb1e | ||
|
|
a63e026700 |
@@ -575,6 +575,9 @@
|
|||||||
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open
|
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open
|
||||||
Folder</span>
|
Folder</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="logs-action-btn logs-send-btn" id="sendLogsBtn" onclick="sendLogs()">
|
||||||
|
<i class="fas fa-paper-plane"></i> <span data-i18n="settings.logsSend">Send Logs</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="logsTerminal" class="logs-terminal">
|
<div id="logsTerminal" class="logs-terminal">
|
||||||
|
|||||||
109
GUI/js/logs.js
109
GUI/js/logs.js
@@ -66,6 +66,113 @@ async function openLogsFolder() {
|
|||||||
await window.electronAPI.openLogsFolder();
|
await window.electronAPI.openLogsFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendLogs() {
|
||||||
|
const btn = document.getElementById('sendLogsBtn');
|
||||||
|
if (!btn || btn.disabled) return;
|
||||||
|
|
||||||
|
// Get i18n strings with fallbacks
|
||||||
|
const i18n = window.i18n || {};
|
||||||
|
const sendingText = (i18n.settings && i18n.settings.logsSending) || 'Sending...';
|
||||||
|
const sentText = (i18n.settings && i18n.settings.logsSent) || 'Sent!';
|
||||||
|
const failedText = (i18n.settings && i18n.settings.logsSendFailed) || 'Failed';
|
||||||
|
const sendText = (i18n.settings && i18n.settings.logsSend) || 'Send Logs';
|
||||||
|
|
||||||
|
const originalHTML = btn.innerHTML;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${sendingText}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.sendLogs();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
btn.innerHTML = `<i class="fas fa-check"></i> ${sentText}`;
|
||||||
|
showLogSubmissionResult(result.id);
|
||||||
|
} else {
|
||||||
|
btn.innerHTML = `<i class="fas fa-times"></i> ${failedText}`;
|
||||||
|
console.error('Send logs failed:', result.error);
|
||||||
|
|
||||||
|
// Show error notification if available
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showNotification) {
|
||||||
|
window.LauncherUI.showNotification(result.error || 'Failed to send logs', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Send logs error:', err);
|
||||||
|
btn.innerHTML = `<i class="fas fa-times"></i> ${failedText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = originalHTML;
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLogSubmissionResult(id) {
|
||||||
|
// Remove existing popup if any
|
||||||
|
const existing = document.getElementById('logSubmissionPopup');
|
||||||
|
if (existing) existing.remove();
|
||||||
|
|
||||||
|
const i18n = window.i18n || {};
|
||||||
|
const idLabel = (i18n.settings && i18n.settings.logsSubmissionId) || 'Submission ID';
|
||||||
|
const copyText = (i18n.common && i18n.common.copy) || 'Copy';
|
||||||
|
const closeText = (i18n.common && i18n.common.close) || 'Close';
|
||||||
|
const shareText = (i18n.settings && i18n.settings.logsShareId) || 'Share this ID with support when reporting issues';
|
||||||
|
|
||||||
|
const popup = document.createElement('div');
|
||||||
|
popup.id = 'logSubmissionPopup';
|
||||||
|
popup.style.cssText = `
|
||||||
|
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||||
|
background: rgba(20, 20, 35, 0.98); border: 1px solid rgba(0, 212, 255, 0.3);
|
||||||
|
border-radius: 12px; padding: 24px 32px; z-index: 10000;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.5); text-align: center;
|
||||||
|
min-width: 320px; backdrop-filter: blur(10px);
|
||||||
|
`;
|
||||||
|
|
||||||
|
popup.innerHTML = `
|
||||||
|
<div style="margin-bottom: 16px;">
|
||||||
|
<i class="fas fa-check-circle" style="font-size: 2em; color: #00d4ff;"></i>
|
||||||
|
</div>
|
||||||
|
<div style="color: #888; font-size: 0.85em; margin-bottom: 8px;">${idLabel}</div>
|
||||||
|
<div id="logSubId" style="font-family: monospace; font-size: 1.5em; color: #00d4ff; letter-spacing: 2px; margin-bottom: 12px; user-select: all;">${id}</div>
|
||||||
|
<div style="color: #666; font-size: 0.8em; margin-bottom: 20px;">${shareText}</div>
|
||||||
|
<div style="display: flex; gap: 10px; justify-content: center;">
|
||||||
|
<button onclick="copyLogSubmissionId('${id}')" style="
|
||||||
|
background: rgba(0,212,255,0.2); border: 1px solid rgba(0,212,255,0.3);
|
||||||
|
color: #00d4ff; padding: 8px 20px; border-radius: 6px; cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
"><i class="fas fa-copy"></i> ${copyText}</button>
|
||||||
|
<button onclick="document.getElementById('logSubmissionPopup').remove()" style="
|
||||||
|
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
color: #ccc; padding: 8px 20px; border-radius: 6px; cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
">${closeText}</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(popup);
|
||||||
|
|
||||||
|
// Auto-close after 30s
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.getElementById('logSubmissionPopup')) {
|
||||||
|
popup.remove();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyLogSubmissionId(id) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(id);
|
||||||
|
const btn = event.target.closest('button');
|
||||||
|
if (btn) {
|
||||||
|
const orig = btn.innerHTML;
|
||||||
|
btn.innerHTML = '<i class="fas fa-check"></i> Copied!';
|
||||||
|
setTimeout(() => { btn.innerHTML = orig; }, 1500);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy submission ID:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openLogs() {
|
function openLogs() {
|
||||||
// Navigation is handled by sidebar logic, but we can trigger a refresh
|
// Navigation is handled by sidebar logic, but we can trigger a refresh
|
||||||
window.LauncherUI.showPage('logs-page');
|
window.LauncherUI.showPage('logs-page');
|
||||||
@@ -77,6 +184,8 @@ function openLogs() {
|
|||||||
window.refreshLogs = refreshLogs;
|
window.refreshLogs = refreshLogs;
|
||||||
window.copyLogs = copyLogs;
|
window.copyLogs = copyLogs;
|
||||||
window.openLogsFolder = openLogsFolder;
|
window.openLogsFolder = openLogsFolder;
|
||||||
|
window.sendLogs = sendLogs;
|
||||||
|
window.copyLogSubmissionId = copyLogSubmissionId;
|
||||||
window.openLogs = openLogs;
|
window.openLogs = openLogs;
|
||||||
|
|
||||||
// Auto-load logs when the page becomes active
|
// Auto-load logs when the page becomes active
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
"logsCopy": "نسخ",
|
"logsCopy": "نسخ",
|
||||||
"logsRefresh": "تحديث",
|
"logsRefresh": "تحديث",
|
||||||
"logsFolder": "فتح المجلد",
|
"logsFolder": "فتح المجلد",
|
||||||
|
"logsSend": "إرسال السجلات",
|
||||||
|
"logsSending": "جارٍ الإرسال...",
|
||||||
|
"logsSent": "تم الإرسال!",
|
||||||
|
"logsSendFailed": "فشل",
|
||||||
|
"logsSubmissionId": "معرف الإرسال",
|
||||||
|
"logsShareId": "شارك هذا المعرف مع الدعم عند الإبلاغ عن المشاكل",
|
||||||
"logsLoading": "جاري تحميل السجلات...",
|
"logsLoading": "جاري تحميل السجلات...",
|
||||||
"closeLauncher": "سلوك المشغل",
|
"closeLauncher": "سلوك المشغل",
|
||||||
"closeOnStart": "إغلاق المشغل عند بدء اللعبة",
|
"closeOnStart": "إغلاق المشغل عند بدء اللعبة",
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
"logsCopy": "Kopieren",
|
"logsCopy": "Kopieren",
|
||||||
"logsRefresh": "Aktualisieren",
|
"logsRefresh": "Aktualisieren",
|
||||||
"logsFolder": "Ordner öffnen",
|
"logsFolder": "Ordner öffnen",
|
||||||
|
"logsSend": "Logs senden",
|
||||||
|
"logsSending": "Senden...",
|
||||||
|
"logsSent": "Gesendet!",
|
||||||
|
"logsSendFailed": "Fehlgeschlagen",
|
||||||
|
"logsSubmissionId": "Einreichungs-ID",
|
||||||
|
"logsShareId": "Teilen Sie diese ID dem Support mit, wenn Sie Probleme melden",
|
||||||
"logsLoading": "Protokolle werden geladen...",
|
"logsLoading": "Protokolle werden geladen...",
|
||||||
"closeLauncher": "Launcher-Verhalten",
|
"closeLauncher": "Launcher-Verhalten",
|
||||||
"closeOnStart": "Launcher beim Spielstart schließen",
|
"closeOnStart": "Launcher beim Spielstart schließen",
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
"logsCopy": "Copy",
|
"logsCopy": "Copy",
|
||||||
"logsRefresh": "Refresh",
|
"logsRefresh": "Refresh",
|
||||||
"logsFolder": "Open Folder",
|
"logsFolder": "Open Folder",
|
||||||
|
"logsSend": "Send Logs",
|
||||||
|
"logsSending": "Sending...",
|
||||||
|
"logsSent": "Sent!",
|
||||||
|
"logsSendFailed": "Failed",
|
||||||
|
"logsSubmissionId": "Submission ID",
|
||||||
|
"logsShareId": "Share this ID with support when reporting issues",
|
||||||
"logsLoading": "Loading logs...",
|
"logsLoading": "Loading logs...",
|
||||||
"closeLauncher": "Launcher Behavior",
|
"closeLauncher": "Launcher Behavior",
|
||||||
"closeOnStart": "Close Launcher on game start",
|
"closeOnStart": "Close Launcher on game start",
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
"logsCopy": "Copiar",
|
"logsCopy": "Copiar",
|
||||||
"logsRefresh": "Actualizar",
|
"logsRefresh": "Actualizar",
|
||||||
"logsFolder": "Abrir Carpeta",
|
"logsFolder": "Abrir Carpeta",
|
||||||
|
"logsSend": "Enviar logs",
|
||||||
|
"logsSending": "Enviando...",
|
||||||
|
"logsSent": "Enviado!",
|
||||||
|
"logsSendFailed": "Fallido",
|
||||||
|
"logsSubmissionId": "ID de envío",
|
||||||
|
"logsShareId": "Comparte este ID con soporte al reportar problemas",
|
||||||
"logsLoading": "Cargando registros...",
|
"logsLoading": "Cargando registros...",
|
||||||
"closeLauncher": "Comportamiento del Launcher",
|
"closeLauncher": "Comportamiento del Launcher",
|
||||||
"closeOnStart": "Cerrar Launcher al iniciar el juego",
|
"closeOnStart": "Cerrar Launcher al iniciar el juego",
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
"logsCopy": "Copier",
|
"logsCopy": "Copier",
|
||||||
"logsRefresh": "Actualiser",
|
"logsRefresh": "Actualiser",
|
||||||
"logsFolder": "Ouvrir le Dossier",
|
"logsFolder": "Ouvrir le Dossier",
|
||||||
|
"logsSend": "Envoyer les logs",
|
||||||
|
"logsSending": "Envoi...",
|
||||||
|
"logsSent": "Envoyé !",
|
||||||
|
"logsSendFailed": "Échoué",
|
||||||
|
"logsSubmissionId": "ID de soumission",
|
||||||
|
"logsShareId": "Partagez cet ID avec le support pour signaler des problèmes",
|
||||||
"logsLoading": "Chargement des journaux...",
|
"logsLoading": "Chargement des journaux...",
|
||||||
"closeLauncher": "Comportement du Launcher",
|
"closeLauncher": "Comportement du Launcher",
|
||||||
"closeOnStart": "Fermer le Launcher au démarrage du jeu",
|
"closeOnStart": "Fermer le Launcher au démarrage du jeu",
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
"logsCopy": "Salin",
|
"logsCopy": "Salin",
|
||||||
"logsRefresh": "Segarkan",
|
"logsRefresh": "Segarkan",
|
||||||
"logsFolder": "Buka Folder",
|
"logsFolder": "Buka Folder",
|
||||||
|
"logsSend": "Kirim Log",
|
||||||
|
"logsSending": "Mengirim...",
|
||||||
|
"logsSent": "Terkirim!",
|
||||||
|
"logsSendFailed": "Gagal",
|
||||||
|
"logsSubmissionId": "ID Pengiriman",
|
||||||
|
"logsShareId": "Bagikan ID ini ke dukungan saat melaporkan masalah",
|
||||||
"logsLoading": "Memuat log...",
|
"logsLoading": "Memuat log...",
|
||||||
"closeLauncher": "Perilaku Launcher",
|
"closeLauncher": "Perilaku Launcher",
|
||||||
"closeOnStart": "Tutup launcher saat game dimulai",
|
"closeOnStart": "Tutup launcher saat game dimulai",
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
"logsCopy": "Kopiuj",
|
"logsCopy": "Kopiuj",
|
||||||
"logsRefresh": "Odśwież",
|
"logsRefresh": "Odśwież",
|
||||||
"logsFolder": "Otwórz Folder",
|
"logsFolder": "Otwórz Folder",
|
||||||
|
"logsSend": "Wyślij logi",
|
||||||
|
"logsSending": "Wysyłanie...",
|
||||||
|
"logsSent": "Wysłano!",
|
||||||
|
"logsSendFailed": "Błąd",
|
||||||
|
"logsSubmissionId": "ID zgłoszenia",
|
||||||
|
"logsShareId": "Udostępnij ten ID wsparciu technicznemu przy zgłaszaniu problemów",
|
||||||
"logsLoading": "Ładowanie logów...",
|
"logsLoading": "Ładowanie logów...",
|
||||||
"closeLauncher": "Zachowanie Launchera",
|
"closeLauncher": "Zachowanie Launchera",
|
||||||
"closeOnStart": "Zamknij Launcher przy starcie gry",
|
"closeOnStart": "Zamknij Launcher przy starcie gry",
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
"logsCopy": "Copiar",
|
"logsCopy": "Copiar",
|
||||||
"logsRefresh": "Atualizar",
|
"logsRefresh": "Atualizar",
|
||||||
"logsFolder": "Abrir Pasta",
|
"logsFolder": "Abrir Pasta",
|
||||||
|
"logsSend": "Enviar logs",
|
||||||
|
"logsSending": "Enviando...",
|
||||||
|
"logsSent": "Enviado!",
|
||||||
|
"logsSendFailed": "Falhou",
|
||||||
|
"logsSubmissionId": "ID de envio",
|
||||||
|
"logsShareId": "Compartilhe este ID com o suporte ao relatar problemas",
|
||||||
"logsLoading": "Carregando registros...",
|
"logsLoading": "Carregando registros...",
|
||||||
"closeLauncher": "Comportamento do Lançador",
|
"closeLauncher": "Comportamento do Lançador",
|
||||||
"closeOnStart": "Fechar Lançador ao iniciar o jogo",
|
"closeOnStart": "Fechar Lançador ao iniciar o jogo",
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
"logsCopy": "Копировать",
|
"logsCopy": "Копировать",
|
||||||
"logsRefresh": "Обновить",
|
"logsRefresh": "Обновить",
|
||||||
"logsFolder": "Открыть папку",
|
"logsFolder": "Открыть папку",
|
||||||
|
"logsSend": "Отправить логи",
|
||||||
|
"logsSending": "Отправка...",
|
||||||
|
"logsSent": "Отправлено!",
|
||||||
|
"logsSendFailed": "Ошибка",
|
||||||
|
"logsSubmissionId": "ID отправки",
|
||||||
|
"logsShareId": "Поделитесь этим ID с поддержкой при обращении",
|
||||||
"logsLoading": "Загрузка логов...",
|
"logsLoading": "Загрузка логов...",
|
||||||
"closeLauncher": "Поведение лаунчера",
|
"closeLauncher": "Поведение лаунчера",
|
||||||
"closeOnStart": "Закрыть лаунчер при старте игры",
|
"closeOnStart": "Закрыть лаунчер при старте игры",
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
"logsCopy": "Kopiera",
|
"logsCopy": "Kopiera",
|
||||||
"logsRefresh": "Uppdatera",
|
"logsRefresh": "Uppdatera",
|
||||||
"logsFolder": "Öppna mapp",
|
"logsFolder": "Öppna mapp",
|
||||||
|
"logsSend": "Skicka loggar",
|
||||||
|
"logsSending": "Skickar...",
|
||||||
|
"logsSent": "Skickat!",
|
||||||
|
"logsSendFailed": "Misslyckades",
|
||||||
|
"logsSubmissionId": "Inlämnings-ID",
|
||||||
|
"logsShareId": "Dela detta ID med support vid felanmälan",
|
||||||
"logsLoading": "Laddar loggar...",
|
"logsLoading": "Laddar loggar...",
|
||||||
"closeLauncher": "Launcher-beteende",
|
"closeLauncher": "Launcher-beteende",
|
||||||
"closeOnStart": "Stäng launcher vid spelstart",
|
"closeOnStart": "Stäng launcher vid spelstart",
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
"logsCopy": "Kopyala",
|
"logsCopy": "Kopyala",
|
||||||
"logsRefresh": "Yenile",
|
"logsRefresh": "Yenile",
|
||||||
"logsFolder": "Klasörü Aç",
|
"logsFolder": "Klasörü Aç",
|
||||||
|
"logsSend": "Logları Gönder",
|
||||||
|
"logsSending": "Gönderiliyor...",
|
||||||
|
"logsSent": "Gönderildi!",
|
||||||
|
"logsSendFailed": "Başarısız",
|
||||||
|
"logsSubmissionId": "Gönderim ID",
|
||||||
|
"logsShareId": "Sorun bildirirken bu ID'yi destekle paylaşın",
|
||||||
"logsLoading": "Loglar yükleniyor...",
|
"logsLoading": "Loglar yükleniyor...",
|
||||||
"closeLauncher": "Başlatıcı Davranışı",
|
"closeLauncher": "Başlatıcı Davranışı",
|
||||||
"closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat",
|
"closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat",
|
||||||
|
|||||||
@@ -873,6 +873,22 @@ body {
|
|||||||
border-color: rgba(255, 255, 255, 0.2);
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs-action-btn.logs-send-btn {
|
||||||
|
background: rgba(0, 212, 255, 0.15);
|
||||||
|
border-color: rgba(0, 212, 255, 0.3);
|
||||||
|
color: #00d4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-action-btn.logs-send-btn:hover {
|
||||||
|
background: rgba(0, 212, 255, 0.25);
|
||||||
|
border-color: rgba(0, 212, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-action-btn.logs-send-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.logs-terminal {
|
.logs-terminal {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: #0d1117;
|
background: #0d1117;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const { ensureGameInstalled } = require('./differentialUpdateManager');
|
|||||||
const { syncModsForCurrentProfile } = require('./modManager');
|
const { syncModsForCurrentProfile } = require('./modManager');
|
||||||
const { getUserDataPath } = require('../utils/userDataMigration');
|
const { getUserDataPath } = require('../utils/userDataMigration');
|
||||||
const { syncServerList } = require('../utils/serverListSync');
|
const { syncServerList } = require('../utils/serverListSync');
|
||||||
|
const { killGameProcesses } = require('./gameManager');
|
||||||
|
|
||||||
// Client patcher for custom auth server (sanasol.ws)
|
// Client patcher for custom auth server (sanasol.ws)
|
||||||
let clientPatcher = null;
|
let clientPatcher = null;
|
||||||
@@ -436,6 +437,22 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kill any stalled game processes from a previous launch to prevent file locks
|
||||||
|
// and "game already running" issues
|
||||||
|
await killGameProcesses();
|
||||||
|
|
||||||
|
// Remove AOT cache: generated by official Hytale JRE, incompatible with F2P JRE.
|
||||||
|
// Client adds -XX:AOTCache when this file exists, causing classloading failures.
|
||||||
|
const aotCache = path.join(gameLatest, 'Server', 'HytaleServer.aot');
|
||||||
|
if (fs.existsSync(aotCache)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(aotCache);
|
||||||
|
console.log('Removed incompatible AOT cache (HytaleServer.aot)');
|
||||||
|
} catch (aotErr) {
|
||||||
|
console.warn('Could not remove AOT cache:', aotErr.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DualAuth Agent: Set JAVA_TOOL_OPTIONS so java picks up -javaagent: flag
|
// DualAuth Agent: Set JAVA_TOOL_OPTIONS so java picks up -javaagent: flag
|
||||||
// This enables runtime auth patching without modifying the server JAR
|
// This enables runtime auth patching without modifying the server JAR
|
||||||
const agentJar = path.join(gameLatest, 'Server', 'dualauth-agent.jar');
|
const agentJar = path.join(gameLatest, 'Server', 'dualauth-agent.jar');
|
||||||
|
|||||||
@@ -39,6 +39,41 @@ async function isGameRunning() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force-kill stalled game processes to release file locks before repair/reinstall.
|
||||||
|
// Cross-platform: Windows (taskkill/PowerShell), macOS (pkill), Linux (pkill).
|
||||||
|
async function killGameProcesses() {
|
||||||
|
const killed = [];
|
||||||
|
|
||||||
|
async function tryKill(command, label) {
|
||||||
|
try {
|
||||||
|
await execAsync(command);
|
||||||
|
killed.push(label);
|
||||||
|
} catch (_) { /* process not found is expected */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Kill client
|
||||||
|
await tryKill('taskkill /F /IM "HytaleClient.exe" /T', 'HytaleClient.exe');
|
||||||
|
// Kill java.exe instances running HytaleServer.jar via PowerShell
|
||||||
|
// (Get-CimInstance replaces deprecated wmic, works on Windows 10+)
|
||||||
|
await tryKill(
|
||||||
|
'powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name=\'java.exe\'\\" | Where-Object { $_.CommandLine -like \'*HytaleServer*\' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"',
|
||||||
|
'java.exe(HytaleServer)'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// macOS and Linux
|
||||||
|
await tryKill('pkill -9 -f HytaleClient', 'HytaleClient');
|
||||||
|
await tryKill('pkill -9 -f HytaleServer', 'HytaleServer');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (killed.length > 0) {
|
||||||
|
console.log(`[GameManager] Force-killed stalled processes: ${killed.join(', ')}`);
|
||||||
|
// Wait for OS to release file handles
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
}
|
||||||
|
return killed;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to safely remove directory with retry logic
|
// Helper function to safely remove directory with retry logic
|
||||||
async function safeRemoveDirectory(dirPath, maxRetries = 3) {
|
async function safeRemoveDirectory(dirPath, maxRetries = 3) {
|
||||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
@@ -52,6 +87,11 @@ async function safeRemoveDirectory(dirPath, maxRetries = 3) {
|
|||||||
console.warn(`Attempt ${attempt}/${maxRetries} failed to remove ${dirPath}: ${error.message}`);
|
console.warn(`Attempt ${attempt}/${maxRetries} failed to remove ${dirPath}: ${error.message}`);
|
||||||
|
|
||||||
if (attempt < maxRetries) {
|
if (attempt < maxRetries) {
|
||||||
|
// On EPERM/EBUSY, try killing stalled game processes that hold file locks
|
||||||
|
if (attempt === 1 && (error.code === 'EPERM' || error.code === 'EBUSY')) {
|
||||||
|
console.log('Permission error detected, killing stalled game processes...');
|
||||||
|
await killGameProcesses();
|
||||||
|
}
|
||||||
// Wait before retrying (exponential backoff)
|
// Wait before retrying (exponential backoff)
|
||||||
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
|
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
|
||||||
console.log(`Waiting ${delay}ms before retry...`);
|
console.log(`Waiting ${delay}ms before retry...`);
|
||||||
@@ -833,11 +873,14 @@ async function repairGame(progressCallback, branchOverride = null) {
|
|||||||
progressCallback('Removing old game files...', 30, null, null, null);
|
progressCallback('Removing old game files...', 30, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if game is running before attempting to delete files
|
// Kill stalled game processes before attempting to delete files
|
||||||
const gameRunning = await isGameRunning();
|
const gameRunning = await isGameRunning();
|
||||||
if (gameRunning) {
|
if (gameRunning) {
|
||||||
console.warn('[RepairGame] Game appears to be running. This may cause permission errors during repair.');
|
console.warn('[RepairGame] Game processes detected. Force-killing to release file locks...');
|
||||||
console.log('[RepairGame] Please close the game before repairing, or wait for the repair to complete.');
|
if (progressCallback) {
|
||||||
|
progressCallback('Stopping stalled game processes...', 20, null, null, null);
|
||||||
|
}
|
||||||
|
await killGameProcesses();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete Game and Cache Directory with retry logic
|
// Delete Game and Cache Directory with retry logic
|
||||||
@@ -964,5 +1007,6 @@ module.exports = {
|
|||||||
installGame,
|
installGame,
|
||||||
uninstallGame,
|
uninstallGame,
|
||||||
checkExistingGameInstallation,
|
checkExistingGameInstallation,
|
||||||
repairGame
|
repairGame,
|
||||||
|
killGameProcesses
|
||||||
};
|
};
|
||||||
|
|||||||
238
backend/utils/logCollector.js
Normal file
238
backend/utils/logCollector.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
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 };
|
||||||
@@ -164,8 +164,14 @@ function detectGpuLinux() {
|
|||||||
const isAmd = lowerLine.includes('amd') || lowerLine.includes('radeon') || vendorId === '1002';
|
const isAmd = lowerLine.includes('amd') || lowerLine.includes('radeon') || vendorId === '1002';
|
||||||
const isIntel = lowerLine.includes('intel') || vendorId === '8086';
|
const isIntel = lowerLine.includes('intel') || vendorId === '8086';
|
||||||
|
|
||||||
// Intel Arc detection
|
// Intel Arc discrete GPU detection
|
||||||
const isIntelArc = isIntel && (lowerName.includes('arc') || lowerName.includes('a770') || lowerName.includes('a750') || lowerName.includes('a380'));
|
// Discrete Arc cards have specific model numbers: A310, A380, A580, A750, A770, B570, B580
|
||||||
|
// Integrated "Intel Arc Graphics" (Meteor Lake, Lunar Lake, Arrow Lake) have NO model suffix
|
||||||
|
// and sit on bus 00 (slot 00:02.0) — these are iGPUs, not dGPUs
|
||||||
|
const pciSlot = line.split(' ')[0] || '';
|
||||||
|
const pciBus = parseInt(pciSlot.split(':')[0], 16) || 0;
|
||||||
|
const hasArcModelNumber = isIntel && /\b[ab]\d{3}\b/i.test(lowerName);
|
||||||
|
const isIntelArc = isIntel && (hasArcModelNumber || (lowerName.includes('arc') && pciBus > 0));
|
||||||
|
|
||||||
let vendor = 'unknown';
|
let vendor = 'unknown';
|
||||||
if (isNvidia) vendor = 'nvidia';
|
if (isNvidia) vendor = 'nvidia';
|
||||||
|
|||||||
77
main.js
77
main.js
@@ -1407,6 +1407,83 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ipcMain.handle('send-logs', async () => {
|
||||||
|
try {
|
||||||
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
|
const { collectLogs, createZipBuffer } = require('./backend/utils/logCollector');
|
||||||
|
|
||||||
|
const { files, meta } = collectLogs();
|
||||||
|
if (files.length === 0) {
|
||||||
|
return { success: false, error: 'No log files found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ZIP with individual log files
|
||||||
|
const zipBuffer = createZipBuffer(files);
|
||||||
|
|
||||||
|
// Get auth server URL from core config
|
||||||
|
const { getAuthServerUrl } = require('./backend/core/config');
|
||||||
|
const authUrl = getAuthServerUrl();
|
||||||
|
|
||||||
|
// Build file names list
|
||||||
|
const fileNames = files.map(f => f.name).join(',');
|
||||||
|
|
||||||
|
return await new Promise((resolve) => {
|
||||||
|
const url = new URL(authUrl + '/logs/submit');
|
||||||
|
const transport = url.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: url.pathname,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/zip',
|
||||||
|
'Content-Length': zipBuffer.length,
|
||||||
|
'X-Log-Username': meta.username || 'unknown',
|
||||||
|
'X-Log-Platform': meta.platform || 'unknown',
|
||||||
|
'X-Log-Version': meta.version || 'unknown',
|
||||||
|
'X-Log-File-Count': String(files.length),
|
||||||
|
'X-Log-Files': fileNames
|
||||||
|
},
|
||||||
|
timeout: 30000
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = transport.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', (chunk) => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(body);
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
resolve({ success: true, id: data.id, message: data.message });
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: data.error || `Server error ${res.statusCode}` });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resolve({ success: false, error: `Invalid response: ${res.statusCode}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => {
|
||||||
|
resolve({ success: false, error: err.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
resolve({ success: false, error: 'Request timed out' });
|
||||||
|
});
|
||||||
|
|
||||||
|
req.write(zipBuffer);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending logs:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('open-logs-folder', async () => {
|
ipcMain.handle('open-logs-folder', async () => {
|
||||||
try {
|
try {
|
||||||
const logDir = logger.getLogDirectory();
|
const logDir = logger.getLogDirectory();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcher",
|
||||||
"version": "2.3.8",
|
"version": "2.4.0",
|
||||||
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
||||||
"homepage": "https://git.sanhost.net/sanasol/hytale-f2p",
|
"homepage": "https://git.sanhost.net/sanasol/hytale-f2p",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
|
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
|
||||||
openLogsFolder: () => ipcRenderer.invoke('open-logs-folder'),
|
openLogsFolder: () => ipcRenderer.invoke('open-logs-folder'),
|
||||||
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
|
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
|
||||||
|
sendLogs: () => ipcRenderer.invoke('send-logs'),
|
||||||
|
|
||||||
// UUID Management methods
|
// UUID Management methods
|
||||||
getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'),
|
getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'),
|
||||||
|
|||||||
@@ -9,19 +9,38 @@ Host your own Hytale server. The scripts handle everything automatically.
|
|||||||
- **Internet connection** for first launch (downloads ~3.5 GB of game files)
|
- **Internet connection** for first launch (downloads ~3.5 GB of game files)
|
||||||
- If you have the F2P launcher installed, game files are copied locally (no download needed)
|
- If you have the F2P launcher installed, game files are copied locally (no download needed)
|
||||||
|
|
||||||
|
## Video Guide
|
||||||
|
|
||||||
|
[](https://youtu.be/KvuXLH7SKvI)
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Windows
|
### Free Hosted Server (no PC required)
|
||||||
|
|
||||||
|
Use [play.hosting](https://play.hosting) to get a free Hytale server with F2P support:
|
||||||
|
|
||||||
|
1. Register at [play.hosting](https://play.hosting)
|
||||||
|
2. Create a **Hytale** server
|
||||||
|
3. Start the server once and wait for it to fully load
|
||||||
|
4. Go to **Files** → open the `mods` folder
|
||||||
|
5. Click **New** → **File via URL**
|
||||||
|
6. Paste: `https://github.com/sanasol/hytale-auth-server/releases/latest/download/dualauth-agent.jar`
|
||||||
|
7. Click **Query** and download the file
|
||||||
|
8. Go to **Console** and **Restart** the server
|
||||||
|
|
||||||
|
Done — your F2P server is ready to join.
|
||||||
|
|
||||||
|
### Windows (self-hosted)
|
||||||
|
|
||||||
1. Download `start.bat` to an empty folder
|
1. Download `start.bat` to an empty folder
|
||||||
2. Double-click `start.bat`
|
2. Double-click `start.bat`
|
||||||
3. Done — server starts on port **5520**
|
3. Done — server starts on port **5520**
|
||||||
|
|
||||||
### Linux / macOS
|
### Linux / macOS (self-hosted)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir hytale-server && cd hytale-server
|
mkdir hytale-server && cd hytale-server
|
||||||
curl -O https://raw.githubusercontent.com/amiayweb/Hytale-F2P/develop/server/start.sh
|
curl -O https://git.sanhost.net/sanasol/hytale-f2p/raw/branch/develop/server/start.sh
|
||||||
chmod +x start.sh
|
chmod +x start.sh
|
||||||
./start.sh
|
./start.sh
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user