mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 11:41:49 -03:00
Compare commits
12 Commits
a63e026700
...
v2.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4932a7a51c | ||
|
|
5170f453ea | ||
|
|
db3b2fc966 | ||
|
|
2f5820e850 | ||
|
|
4abb455e0f | ||
|
|
d5828463f9 | ||
|
|
0d15659dc0 | ||
|
|
19c8991a44 | ||
|
|
320ca54758 | ||
|
|
e14d56ef48 | ||
|
|
a649bf1fcc | ||
|
|
66faa1bb1e |
@@ -430,8 +430,70 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">
|
||||
<i class="fas fa-scroll"></i>
|
||||
<span data-i18n="settings.wrapperConfig">Java Wrapper Configuration</span>
|
||||
</h3>
|
||||
<p class="settings-hint" style="margin-bottom: 12px;">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span data-i18n="settings.wrapperConfigHint">Configure how the Java wrapper handles JVM flags and arguments at launch time.</span>
|
||||
</p>
|
||||
|
||||
<!-- Strip Flags -->
|
||||
<label class="settings-label" style="margin-bottom: 6px;">
|
||||
<span data-i18n="settings.wrapperStripFlags">JVM Flags to Remove</span>
|
||||
</label>
|
||||
<div id="wrapperStripFlagsList" class="wrapper-items-list"></div>
|
||||
<div style="display: flex; gap: 6px; margin-top: 6px;">
|
||||
<input type="text" id="wrapperAddFlagInput" class="settings-input" style="flex:1;"
|
||||
data-i18n-placeholder="settings.wrapperAddFlagPlaceholder" placeholder="e.g. -XX:+SomeFlag" spellcheck="false">
|
||||
<button id="wrapperAddFlagBtn" class="settings-browse-btn">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span data-i18n="settings.wrapperAdd">Add</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Inject Args -->
|
||||
<label class="settings-label" style="margin-top: 16px; margin-bottom: 6px;">
|
||||
<span data-i18n="settings.wrapperInjectArgs">Arguments to Inject</span>
|
||||
</label>
|
||||
<div id="wrapperInjectArgsList" class="wrapper-items-list"></div>
|
||||
<div style="display: flex; gap: 6px; margin-top: 6px;">
|
||||
<input type="text" id="wrapperAddArgInput" class="settings-input" style="flex:1;"
|
||||
data-i18n-placeholder="settings.wrapperAddArgPlaceholder" placeholder="e.g. --some-flag" spellcheck="false">
|
||||
<select id="wrapperAddArgCondition" class="wrapper-condition-select">
|
||||
<option value="server" data-i18n="settings.wrapperConditionServer">Server Only</option>
|
||||
<option value="always" data-i18n="settings.wrapperConditionAlways">Always</option>
|
||||
</select>
|
||||
<button id="wrapperAddArgBtn" class="settings-browse-btn">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span data-i18n="settings.wrapperAdd">Add</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Restore Defaults -->
|
||||
<div style="margin-top: 12px;">
|
||||
<button id="wrapperRestoreDefaultsBtn" class="settings-browse-btn" style="background: rgba(239, 68, 68, 0.2); border-color: rgba(239, 68, 68, 0.3);">
|
||||
<i class="fas fa-undo"></i>
|
||||
<span data-i18n="settings.wrapperRestoreDefaults">Restore Defaults</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Script Preview (collapsible) -->
|
||||
<div style="margin-top: 12px;">
|
||||
<button id="wrapperPreviewToggle" class="wrapper-preview-toggle">
|
||||
<i class="fas fa-chevron-right" id="wrapperPreviewChevron"></i>
|
||||
<span data-i18n="settings.wrapperAdvancedPreview">Advanced: Script Preview</span>
|
||||
</button>
|
||||
<div id="wrapperPreviewContainer" style="display: none; margin-top: 8px;">
|
||||
<pre id="wrapperPreviewContent" class="wrapper-preview-content"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="settings-column">
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">
|
||||
@@ -575,6 +637,9 @@
|
||||
<i class="fas fa-folder-open"></i> <span data-i18n="settings.logsFolder">Open
|
||||
Folder</span>
|
||||
</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 id="logsTerminal" class="logs-terminal">
|
||||
@@ -598,7 +663,11 @@
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mods-modal-body">
|
||||
<div class="mods-modal-body" style="padding-top: 0;">
|
||||
<div class="mods-search-container" style="margin: 1.5rem; margin-bottom: 1rem;">
|
||||
<i class="fas fa-search"></i>
|
||||
<input type="text" id="myModsSearch" placeholder="Search installed mods..." class="mods-search" />
|
||||
</div>
|
||||
<div id="installedModsList" class="installed-mods-list">
|
||||
</div>
|
||||
</div>
|
||||
@@ -802,6 +871,25 @@
|
||||
<script src="js/featured.js"></script>
|
||||
<script type="module" src="js/settings.js"></script>
|
||||
<script type="module" src="js/update.js"></script>
|
||||
|
||||
<!-- Version Selection Modal (Isolated Container) -->
|
||||
<div id="versionSelectModal" class="modal-overlay" style="display: none; position: fixed; inset: 0; z-index: 9999; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(5px); align-items: center; justify-content: center;">
|
||||
<div class="glass-panel" style="width: 100%; max-width: 600px; max-height: 80vh; display: flex; flex-direction: column; border-radius: 12px; overflow: hidden; margin: 20px;">
|
||||
<div class="modal-header" style="display: flex; justify-content: space-between; align-items: center; padding: 1.5rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1);">
|
||||
<h3 style="margin: 0; font-size: 1.25rem;">Select Version</h3>
|
||||
<button id="closeVersionModal" class="modal-close" style="background: none; border: none; color: #a0a0a0; font-size: 1.25rem; cursor: pointer;"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="modal-body" style="padding: 1.5rem; overflow-y: auto;">
|
||||
<div id="versionList" class="version-list-container">
|
||||
<div class="loading-versions" style="display: flex; flex-direction: column; align-items: center; gap: 1rem; color: #a0a0a0;">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||
<span>Loading versions...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- updater.js disabled - using update.js instead which has skip button and macOS handling -->
|
||||
</body>
|
||||
|
||||
|
||||
109
GUI/js/logs.js
109
GUI/js/logs.js
@@ -66,6 +66,113 @@ async function 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() {
|
||||
// Navigation is handled by sidebar logic, but we can trigger a refresh
|
||||
window.LauncherUI.showPage('logs-page');
|
||||
@@ -77,6 +184,8 @@ function openLogs() {
|
||||
window.refreshLogs = refreshLogs;
|
||||
window.copyLogs = copyLogs;
|
||||
window.openLogsFolder = openLogsFolder;
|
||||
window.sendLogs = sendLogs;
|
||||
window.copyLogSubmissionId = copyLogSubmissionId;
|
||||
window.openLogs = openLogs;
|
||||
|
||||
// Auto-load logs when the page becomes active
|
||||
|
||||
184
GUI/js/mods.js
184
GUI/js/mods.js
@@ -49,6 +49,18 @@ function setupModsEventListeners() {
|
||||
closeModalBtn.addEventListener('click', closeMyModsModal);
|
||||
}
|
||||
|
||||
const myModsSearchInput = document.getElementById('myModsSearch');
|
||||
if (myModsSearchInput) {
|
||||
let myModsSearchTimeout;
|
||||
myModsSearchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase().trim();
|
||||
clearTimeout(myModsSearchTimeout);
|
||||
myModsSearchTimeout = setTimeout(() => {
|
||||
filterInstalledMods(query);
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
|
||||
const modal = document.getElementById('myModsModal');
|
||||
if (modal) {
|
||||
modal.addEventListener('click', (e) => {
|
||||
@@ -78,12 +90,30 @@ function setupModsEventListeners() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const browseContainer = document.getElementById('browseModsList');
|
||||
if (browseContainer) {
|
||||
browseContainer.addEventListener('click', (e) => {
|
||||
const installBtn = e.target.closest('[data-install-mod-id]');
|
||||
if (installBtn) {
|
||||
const modId = installBtn.getAttribute('data-install-mod-id');
|
||||
const mod = browseMods.find(m => m.id == modId);
|
||||
if (mod) {
|
||||
openVersionSelectModal(mod);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function openMyModsModal() {
|
||||
const modal = document.getElementById('myModsModal');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
const searchInput = document.getElementById('myModsSearch');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
loadInstalledMods();
|
||||
}
|
||||
}
|
||||
@@ -92,6 +122,10 @@ function closeMyModsModal() {
|
||||
const modal = document.getElementById('myModsModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
const searchInput = document.getElementById('myModsSearch');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,19 +147,39 @@ async function loadInstalledMods() {
|
||||
}
|
||||
}
|
||||
|
||||
function filterInstalledMods(query) {
|
||||
if (!query || query === '') {
|
||||
displayInstalledMods(installedMods);
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = installedMods.filter(mod => {
|
||||
const nameMatch = mod.name?.toLowerCase().includes(query);
|
||||
const fileNameMatch = mod.fileName?.toLowerCase().includes(query);
|
||||
const descriptionMatch = mod.description?.toLowerCase().includes(query);
|
||||
const authorMatch = mod.author?.toLowerCase().includes(query);
|
||||
return nameMatch || fileNameMatch || descriptionMatch || authorMatch;
|
||||
});
|
||||
|
||||
displayInstalledMods(filtered);
|
||||
}
|
||||
|
||||
function displayInstalledMods(mods) {
|
||||
const modsContainer = document.getElementById('installedModsList');
|
||||
if (!modsContainer) return;
|
||||
|
||||
if (mods.length === 0) {
|
||||
const searchInput = document.getElementById('myModsSearch');
|
||||
const isSearching = searchInput && searchInput.value.trim() !== '';
|
||||
|
||||
modsContainer.innerHTML = `
|
||||
<div class=\"empty-installed-mods\">
|
||||
<i class=\"fas fa-box-open\"></i>
|
||||
<h4 data-i18n="mods.noModsInstalled">No Mods Installed</h4>
|
||||
<p data-i18n="mods.noModsInstalledDesc">Add mods from CurseForge or import local files</p>
|
||||
<i class=\"fas fa-${isSearching ? 'search' : 'box-open'}\"></i>
|
||||
<h4 data-i18n="${isSearching ? 'mods.noModsFound' : 'mods.noModsInstalled'}">${isSearching ? 'No Mods Found' : 'No Mods Installed'}</h4>
|
||||
<p data-i18n="${isSearching ? 'mods.noModsFoundDesc' : 'mods.noModsInstalledDesc'}">${isSearching ? 'Try a different search term' : 'Add mods from CurseForge or import local files'}</p>
|
||||
</div>
|
||||
`;
|
||||
if (window.i18n) {
|
||||
if (window.i18n && !isSearching) {
|
||||
const container = modsContainer.querySelector('.empty-installed-mods');
|
||||
container.querySelector('h4').textContent = window.i18n.t('mods.noModsInstalled');
|
||||
container.querySelector('p').textContent = window.i18n.t('mods.noModsInstalledDesc');
|
||||
@@ -165,7 +219,7 @@ function createInstalledModCard(mod) {
|
||||
<div class="installed-mod-info">
|
||||
<div class="installed-mod-header">
|
||||
<h4 class="installed-mod-name">${mod.name}</h4>
|
||||
<span class="installed-mod-version">v${mod.version}</span>
|
||||
<span class="installed-mod-version">${mod.fileName || 'v' + mod.version}</span>
|
||||
</div>
|
||||
<p class="installed-mod-description">${mod.description || (window.i18n ? window.i18n.t('mods.noDescription') : 'No description available')}</p>
|
||||
</div>
|
||||
@@ -295,13 +349,6 @@ function displayBrowseMods(mods) {
|
||||
}
|
||||
|
||||
browseContainer.innerHTML = mods.map(mod => createBrowseModCard(mod)).join('');
|
||||
|
||||
mods.forEach(mod => {
|
||||
const installBtn = document.getElementById(`install-${mod.id}`);
|
||||
if (installBtn) {
|
||||
installBtn.addEventListener('click', () => downloadAndInstallMod(mod));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createBrowseModCard(mod) {
|
||||
@@ -350,12 +397,12 @@ function createBrowseModCard(mod) {
|
||||
${window.i18n ? window.i18n.t('mods.view') : 'VIEW'}
|
||||
</button>
|
||||
${!isInstalled ?
|
||||
`<button id="install-${mod.id}" class="mod-btn-toggle bg-primary text-black hover:bg-primary/80">
|
||||
<i class="fas fa-download"></i>
|
||||
`<button data-install-mod-id=\"${mod.id}\" class=\"mod-btn-toggle bg-primary text-black hover:bg-primary/80\">
|
||||
<i class=\"fas fa-download\"></i>
|
||||
${window.i18n ? window.i18n.t('mods.install') : 'INSTALL'}
|
||||
</button>` :
|
||||
`<button class="mod-btn-toggle bg-white/10 text-white" disabled>
|
||||
<i class="fas fa-check"></i>
|
||||
`<button class=\"mod-btn-toggle bg-white/10 text-white\" disabled>
|
||||
<i class=\"fas fa-check\"></i>
|
||||
${window.i18n ? window.i18n.t('mods.installed') : 'INSTALLED'}
|
||||
</button>`
|
||||
}
|
||||
@@ -364,6 +411,104 @@ function createBrowseModCard(mod) {
|
||||
`;
|
||||
}
|
||||
|
||||
let currentSelectedMod = null;
|
||||
|
||||
function openVersionSelectModal(mod) {
|
||||
currentSelectedMod = mod;
|
||||
const modal = document.getElementById('versionSelectModal');
|
||||
const closeBtn = document.getElementById('closeVersionModal');
|
||||
const versionList = document.getElementById('versionList');
|
||||
|
||||
if (modal) {
|
||||
modal.style.display = 'flex';
|
||||
modal.classList.add('active');
|
||||
|
||||
const closeHandler = () => {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => {
|
||||
modal.style.display = 'none';
|
||||
}, 300);
|
||||
currentSelectedMod = null;
|
||||
};
|
||||
|
||||
if (closeBtn) {
|
||||
closeBtn.onclick = closeHandler;
|
||||
}
|
||||
modal.onclick = (e) => {
|
||||
if (e.target === modal) closeHandler();
|
||||
};
|
||||
|
||||
loadModVersions(mod.id, versionList);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadModVersions(modId, container) {
|
||||
container.innerHTML = `
|
||||
<div class="loading-versions">
|
||||
<i class="fas fa-spinner fa-spin fa-2x" style="margin-bottom: 10px; display: block;"></i>
|
||||
<span>Loading versions...</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const versions = await window.electronAPI.getModFiles(modId);
|
||||
|
||||
if (!versions || versions.length === 0) {
|
||||
container.innerHTML = `<div class="p-4 text-center text-gray-400" style="padding: 2rem;">No versions found for this mod.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort versions by date desc (API returns desc but ensure)
|
||||
versions.sort((a, b) => new Date(b.fileDate) - new Date(a.fileDate));
|
||||
|
||||
container.innerHTML = versions.map(file => `
|
||||
<div class="version-item">
|
||||
<div class="version-info">
|
||||
<div class="version-name">${file.displayName}</div>
|
||||
<div class="version-meta">
|
||||
<span><i class="fas fa-calendar"></i> ${new Date(file.fileDate).toLocaleDateString()}</span>
|
||||
<span><i class="fas fa-download"></i> ${formatNumber(file.downloadCount)}</span>
|
||||
<span><i class="fas fa-file-archive"></i> ${(file.fileLength / 1024 / 1024).toFixed(2)} MB</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-actions">
|
||||
<button class="btn-install" data-file-id="${file.id}">
|
||||
Install
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add event listeners securely
|
||||
container.querySelectorAll('.btn-install').forEach((btn, index) => {
|
||||
const file = versions[index]; // Map index to file data
|
||||
btn.onclick = () => installVersion(file);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading versions:', error);
|
||||
container.innerHTML = `<div class="p-4 text-center text-red-400" style="padding: 2rem;">Error loading versions.<br><small>${error.message}</small></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function installVersion(file) {
|
||||
if (!currentSelectedMod) return;
|
||||
|
||||
const modal = document.getElementById('versionSelectModal');
|
||||
modal.style.display = 'none';
|
||||
|
||||
const modInfo = {
|
||||
...currentSelectedMod,
|
||||
fileId: file.id,
|
||||
downloadUrl: file.downloadUrl,
|
||||
fileName: file.fileName,
|
||||
fileSize: file.fileLength
|
||||
};
|
||||
|
||||
await downloadAndInstallMod(modInfo);
|
||||
currentSelectedMod = null;
|
||||
}
|
||||
|
||||
async function downloadAndInstallMod(modInfo) {
|
||||
try {
|
||||
const downloadMsg = window.i18n ? window.i18n.t('notifications.modsDownloading').replace('{name}', modInfo.name) : `Downloading ${modInfo.name}...`;
|
||||
@@ -762,7 +907,10 @@ window.modsManager = {
|
||||
closeMyModsModal,
|
||||
viewModPage,
|
||||
loadInstalledMods,
|
||||
loadBrowseMods
|
||||
loadBrowseMods,
|
||||
openVersionSelectModal
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initModsManager);
|
||||
// Remove auto-init since we are now calling it from script.js explicitly
|
||||
// which guarantees order and environment readiness
|
||||
// document.addEventListener('DOMContentLoaded', initModsManager);
|
||||
|
||||
@@ -2,7 +2,7 @@ import './ui.js';
|
||||
import './install.js';
|
||||
import './launcher.js';
|
||||
import './news.js';
|
||||
import './mods.js';
|
||||
import { initModsManager } from './mods.js';
|
||||
import './players.js';
|
||||
import './settings.js';
|
||||
import './logs.js';
|
||||
@@ -15,6 +15,12 @@ let i18nInitialized = false;
|
||||
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
updateLanguageSelector();
|
||||
initModsManager();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateLanguageSelector();
|
||||
initModsManager();
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
@@ -569,6 +569,7 @@ async function loadAllSettings() {
|
||||
await loadLauncherHwAccel();
|
||||
await loadGpuPreference();
|
||||
await loadVersionBranch();
|
||||
await loadWrapperConfigUI();
|
||||
}
|
||||
|
||||
|
||||
@@ -1254,3 +1255,235 @@ async function loadVersionBranch() {
|
||||
return 'release';
|
||||
}
|
||||
}
|
||||
|
||||
// === Java Wrapper Configuration UI ===
|
||||
|
||||
let _wrapperConfig = null;
|
||||
let _wrapperPreviewOpen = false;
|
||||
|
||||
async function loadWrapperConfigUI() {
|
||||
try {
|
||||
if (!window.electronAPI || !window.electronAPI.loadWrapperConfig) return;
|
||||
|
||||
_wrapperConfig = await window.electronAPI.loadWrapperConfig();
|
||||
renderStripFlagsList();
|
||||
renderInjectArgsList();
|
||||
setupWrapperEventListeners();
|
||||
} catch (error) {
|
||||
console.error('Error loading wrapper config UI:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderStripFlagsList() {
|
||||
const container = document.getElementById('wrapperStripFlagsList');
|
||||
if (!container || !_wrapperConfig) return;
|
||||
|
||||
if (_wrapperConfig.stripFlags.length === 0) {
|
||||
container.innerHTML = '<div class="wrapper-items-empty">No flags configured</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
_wrapperConfig.stripFlags.forEach((flag, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'wrapper-item';
|
||||
item.innerHTML = `
|
||||
<span class="wrapper-item-text">${escapeHtml(flag)}</span>
|
||||
<button class="wrapper-item-delete" data-index="${index}" title="Remove">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
`;
|
||||
item.querySelector('.wrapper-item-delete').addEventListener('click', () => removeStripFlag(index));
|
||||
container.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function renderInjectArgsList() {
|
||||
const container = document.getElementById('wrapperInjectArgsList');
|
||||
if (!container || !_wrapperConfig) return;
|
||||
|
||||
if (_wrapperConfig.injectArgs.length === 0) {
|
||||
container.innerHTML = '<div class="wrapper-items-empty">No arguments configured</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
_wrapperConfig.injectArgs.forEach((entry, index) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'wrapper-item';
|
||||
|
||||
const serverLabel = window.i18n ? window.i18n.t('settings.wrapperConditionServer') : 'Server Only';
|
||||
const alwaysLabel = window.i18n ? window.i18n.t('settings.wrapperConditionAlways') : 'Always';
|
||||
|
||||
item.innerHTML = `
|
||||
<span class="wrapper-item-text">${escapeHtml(entry.arg)}</span>
|
||||
<div class="wrapper-item-condition">
|
||||
<select data-index="${index}">
|
||||
<option value="server"${entry.condition === 'server' ? ' selected' : ''}>${serverLabel}</option>
|
||||
<option value="always"${entry.condition === 'always' ? ' selected' : ''}>${alwaysLabel}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="wrapper-item-delete" data-index="${index}" title="Remove">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
`;
|
||||
item.querySelector('select').addEventListener('change', (e) => updateArgCondition(index, e.target.value));
|
||||
item.querySelector('.wrapper-item-delete').addEventListener('click', () => removeInjectArg(index));
|
||||
container.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async function addStripFlag() {
|
||||
const input = document.getElementById('wrapperAddFlagInput');
|
||||
if (!input || !_wrapperConfig) return;
|
||||
|
||||
const flag = input.value.trim();
|
||||
if (!flag) return;
|
||||
|
||||
if (_wrapperConfig.stripFlags.includes(flag)) {
|
||||
const msg = window.i18n ? window.i18n.t('notifications.wrapperFlagExists') : 'This flag is already in the list';
|
||||
showNotification(msg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
_wrapperConfig.stripFlags.push(flag);
|
||||
input.value = '';
|
||||
renderStripFlagsList();
|
||||
await saveWrapperConfigToBackend();
|
||||
await updateWrapperPreview();
|
||||
}
|
||||
|
||||
async function removeStripFlag(index) {
|
||||
if (!_wrapperConfig) return;
|
||||
_wrapperConfig.stripFlags.splice(index, 1);
|
||||
renderStripFlagsList();
|
||||
await saveWrapperConfigToBackend();
|
||||
await updateWrapperPreview();
|
||||
}
|
||||
|
||||
async function addInjectArg() {
|
||||
const input = document.getElementById('wrapperAddArgInput');
|
||||
const condSelect = document.getElementById('wrapperAddArgCondition');
|
||||
if (!input || !condSelect || !_wrapperConfig) return;
|
||||
|
||||
const arg = input.value.trim();
|
||||
if (!arg) return;
|
||||
|
||||
const exists = _wrapperConfig.injectArgs.some(e => e.arg === arg);
|
||||
if (exists) {
|
||||
const msg = window.i18n ? window.i18n.t('notifications.wrapperArgExists') : 'This argument is already in the list';
|
||||
showNotification(msg, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
_wrapperConfig.injectArgs.push({ arg, condition: condSelect.value });
|
||||
input.value = '';
|
||||
renderInjectArgsList();
|
||||
await saveWrapperConfigToBackend();
|
||||
await updateWrapperPreview();
|
||||
}
|
||||
|
||||
async function removeInjectArg(index) {
|
||||
if (!_wrapperConfig) return;
|
||||
_wrapperConfig.injectArgs.splice(index, 1);
|
||||
renderInjectArgsList();
|
||||
await saveWrapperConfigToBackend();
|
||||
await updateWrapperPreview();
|
||||
}
|
||||
|
||||
async function updateArgCondition(index, condition) {
|
||||
if (!_wrapperConfig || !_wrapperConfig.injectArgs[index]) return;
|
||||
_wrapperConfig.injectArgs[index].condition = condition;
|
||||
await saveWrapperConfigToBackend();
|
||||
await updateWrapperPreview();
|
||||
}
|
||||
|
||||
async function saveWrapperConfigToBackend() {
|
||||
try {
|
||||
const result = await window.electronAPI.saveWrapperConfig(_wrapperConfig);
|
||||
if (!result || !result.success) {
|
||||
throw new Error(result?.error || 'Save failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving wrapper config:', error);
|
||||
const msg = window.i18n ? window.i18n.t('notifications.wrapperConfigSaveFailed') : 'Failed to save wrapper configuration';
|
||||
showNotification(msg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function setupWrapperEventListeners() {
|
||||
const addFlagBtn = document.getElementById('wrapperAddFlagBtn');
|
||||
const addFlagInput = document.getElementById('wrapperAddFlagInput');
|
||||
const addArgBtn = document.getElementById('wrapperAddArgBtn');
|
||||
const addArgInput = document.getElementById('wrapperAddArgInput');
|
||||
const restoreBtn = document.getElementById('wrapperRestoreDefaultsBtn');
|
||||
const previewToggle = document.getElementById('wrapperPreviewToggle');
|
||||
|
||||
if (addFlagBtn) addFlagBtn.addEventListener('click', addStripFlag);
|
||||
if (addFlagInput) addFlagInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') addStripFlag(); });
|
||||
if (addArgBtn) addArgBtn.addEventListener('click', addInjectArg);
|
||||
if (addArgInput) addArgInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') addInjectArg(); });
|
||||
|
||||
if (restoreBtn) {
|
||||
restoreBtn.addEventListener('click', () => {
|
||||
const message = window.i18n ? window.i18n.t('confirm.resetWrapperMessage') : 'Are you sure you want to restore defaults? Your custom changes will be lost.';
|
||||
const title = window.i18n ? window.i18n.t('confirm.resetWrapperTitle') : 'Restore Defaults';
|
||||
|
||||
showCustomConfirm(message, title, async () => {
|
||||
try {
|
||||
const result = await window.electronAPI.resetWrapperConfig();
|
||||
if (result && result.success) {
|
||||
_wrapperConfig = result.config;
|
||||
renderStripFlagsList();
|
||||
renderInjectArgsList();
|
||||
await updateWrapperPreview();
|
||||
const msg = window.i18n ? window.i18n.t('notifications.wrapperConfigReset') : 'Wrapper configuration restored to defaults';
|
||||
showNotification(msg, 'success');
|
||||
} else {
|
||||
throw new Error(result?.error || 'Reset failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error resetting wrapper config:', error);
|
||||
const msg = window.i18n ? window.i18n.t('notifications.wrapperConfigResetFailed') : 'Failed to restore wrapper configuration';
|
||||
showNotification(msg, 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (previewToggle) {
|
||||
previewToggle.addEventListener('click', toggleWrapperPreview);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleWrapperPreview() {
|
||||
const container = document.getElementById('wrapperPreviewContainer');
|
||||
const chevron = document.getElementById('wrapperPreviewChevron');
|
||||
if (!container) return;
|
||||
|
||||
_wrapperPreviewOpen = !_wrapperPreviewOpen;
|
||||
|
||||
if (_wrapperPreviewOpen) {
|
||||
container.style.display = 'block';
|
||||
if (chevron) chevron.classList.add('expanded');
|
||||
await updateWrapperPreview();
|
||||
} else {
|
||||
container.style.display = 'none';
|
||||
if (chevron) chevron.classList.remove('expanded');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateWrapperPreview() {
|
||||
if (!_wrapperPreviewOpen || !_wrapperConfig) return;
|
||||
|
||||
const previewEl = document.getElementById('wrapperPreviewContent');
|
||||
if (!previewEl) return;
|
||||
|
||||
try {
|
||||
const platform = await window.electronAPI.getCurrentPlatform();
|
||||
const script = await window.electronAPI.previewWrapperScript(_wrapperConfig, platform);
|
||||
previewEl.textContent = script;
|
||||
} catch (error) {
|
||||
previewEl.textContent = 'Error generating preview: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"logsCopy": "نسخ",
|
||||
"logsRefresh": "تحديث",
|
||||
"logsFolder": "فتح المجلد",
|
||||
"logsSend": "إرسال السجلات",
|
||||
"logsSending": "جارٍ الإرسال...",
|
||||
"logsSent": "تم الإرسال!",
|
||||
"logsSendFailed": "فشل",
|
||||
"logsSubmissionId": "معرف الإرسال",
|
||||
"logsShareId": "شارك هذا المعرف مع الدعم عند الإبلاغ عن المشاكل",
|
||||
"logsLoading": "جاري تحميل السجلات...",
|
||||
"closeLauncher": "سلوك المشغل",
|
||||
"closeOnStart": "إغلاق المشغل عند بدء اللعبة",
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"logsCopy": "Kopieren",
|
||||
"logsRefresh": "Aktualisieren",
|
||||
"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...",
|
||||
"closeLauncher": "Launcher-Verhalten",
|
||||
"closeOnStart": "Launcher beim Spielstart schließen",
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"logsCopy": "Copy",
|
||||
"logsRefresh": "Refresh",
|
||||
"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...",
|
||||
"closeLauncher": "Launcher Behavior",
|
||||
"closeOnStart": "Close Launcher on game start",
|
||||
@@ -141,7 +147,18 @@
|
||||
"branchSwitching": "Switching to {branch}...",
|
||||
"branchSwitched": "Switched to {branch} successfully!",
|
||||
"installRequired": "Installation Required",
|
||||
"branchInstallConfirm": "The game will be installed for the {branch} branch. Continue?"
|
||||
"branchInstallConfirm": "The game will be installed for the {branch} branch. Continue?",
|
||||
"wrapperConfig": "Java Wrapper Configuration",
|
||||
"wrapperConfigHint": "Configure how the Java wrapper handles JVM flags and arguments at launch time.",
|
||||
"wrapperStripFlags": "JVM Flags to Remove",
|
||||
"wrapperInjectArgs": "Arguments to Inject",
|
||||
"wrapperAddFlagPlaceholder": "e.g. -XX:+SomeFlag",
|
||||
"wrapperAddArgPlaceholder": "e.g. --some-flag",
|
||||
"wrapperAdd": "Add",
|
||||
"wrapperConditionServer": "Server Only",
|
||||
"wrapperConditionAlways": "Always",
|
||||
"wrapperRestoreDefaults": "Restore Defaults",
|
||||
"wrapperAdvancedPreview": "Advanced: Script Preview"
|
||||
},
|
||||
"uuid": {
|
||||
"modalTitle": "UUID Management",
|
||||
@@ -215,7 +232,13 @@
|
||||
"noUsername": "No username configured. Please save your username first.",
|
||||
"switchUsernameSuccess": "Switched to \"{username}\" successfully!",
|
||||
"switchUsernameFailed": "Failed to switch username",
|
||||
"playerNameTooLong": "Player name must be 16 characters or less"
|
||||
"playerNameTooLong": "Player name must be 16 characters or less",
|
||||
"wrapperConfigSaved": "Wrapper configuration saved",
|
||||
"wrapperConfigSaveFailed": "Failed to save wrapper configuration",
|
||||
"wrapperConfigReset": "Wrapper configuration restored to defaults",
|
||||
"wrapperConfigResetFailed": "Failed to restore wrapper configuration",
|
||||
"wrapperFlagExists": "This flag is already in the list",
|
||||
"wrapperArgExists": "This argument is already in the list"
|
||||
},
|
||||
"confirm": {
|
||||
"defaultTitle": "Confirm action",
|
||||
@@ -233,7 +256,9 @@
|
||||
"uninstallGameButton": "Uninstall",
|
||||
"switchUsernameTitle": "Switch Identity",
|
||||
"switchUsernameMessage": "Switch to username \"{username}\"? This will change your current player identity.",
|
||||
"switchUsernameButton": "Switch"
|
||||
"switchUsernameButton": "Switch",
|
||||
"resetWrapperTitle": "Restore Defaults",
|
||||
"resetWrapperMessage": "Are you sure you want to restore the default wrapper configuration? Your custom changes will be lost."
|
||||
},
|
||||
"progress": {
|
||||
"initializing": "Initializing...",
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"logsCopy": "Copiar",
|
||||
"logsRefresh": "Actualizar",
|
||||
"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...",
|
||||
"closeLauncher": "Comportamiento del Launcher",
|
||||
"closeOnStart": "Cerrar Launcher al iniciar el juego",
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"logsCopy": "Copier",
|
||||
"logsRefresh": "Actualiser",
|
||||
"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...",
|
||||
"closeLauncher": "Comportement du Launcher",
|
||||
"closeOnStart": "Fermer le Launcher au démarrage du jeu",
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"logsCopy": "Salin",
|
||||
"logsRefresh": "Segarkan",
|
||||
"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...",
|
||||
"closeLauncher": "Perilaku Launcher",
|
||||
"closeOnStart": "Tutup launcher saat game dimulai",
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"logsCopy": "Kopiuj",
|
||||
"logsRefresh": "Odśwież",
|
||||
"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...",
|
||||
"closeLauncher": "Zachowanie Launchera",
|
||||
"closeOnStart": "Zamknij Launcher przy starcie gry",
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"logsCopy": "Copiar",
|
||||
"logsRefresh": "Atualizar",
|
||||
"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...",
|
||||
"closeLauncher": "Comportamento do Lançador",
|
||||
"closeOnStart": "Fechar Lançador ao iniciar o jogo",
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"logsCopy": "Копировать",
|
||||
"logsRefresh": "Обновить",
|
||||
"logsFolder": "Открыть папку",
|
||||
"logsSend": "Отправить логи",
|
||||
"logsSending": "Отправка...",
|
||||
"logsSent": "Отправлено!",
|
||||
"logsSendFailed": "Ошибка",
|
||||
"logsSubmissionId": "ID отправки",
|
||||
"logsShareId": "Поделитесь этим ID с поддержкой при обращении",
|
||||
"logsLoading": "Загрузка логов...",
|
||||
"closeLauncher": "Поведение лаунчера",
|
||||
"closeOnStart": "Закрыть лаунчер при старте игры",
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"logsCopy": "Kopiera",
|
||||
"logsRefresh": "Uppdatera",
|
||||
"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...",
|
||||
"closeLauncher": "Launcher-beteende",
|
||||
"closeOnStart": "Stäng launcher vid spelstart",
|
||||
|
||||
@@ -127,6 +127,12 @@
|
||||
"logsCopy": "Kopyala",
|
||||
"logsRefresh": "Yenile",
|
||||
"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...",
|
||||
"closeLauncher": "Başlatıcı Davranışı",
|
||||
"closeOnStart": "Oyun başlatıldığında Başlatıcıyı Kapat",
|
||||
|
||||
232
GUI/style.css
232
GUI/style.css
@@ -873,6 +873,22 @@ body {
|
||||
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 {
|
||||
flex: 1;
|
||||
background: #0d1117;
|
||||
@@ -4813,6 +4829,140 @@ select.settings-input option {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.wrapper-items-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wrapper-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.wrapper-item:hover {
|
||||
border-color: rgba(147, 51, 234, 0.3);
|
||||
background: rgba(147, 51, 234, 0.05);
|
||||
}
|
||||
|
||||
.wrapper-item-text {
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 0.82rem;
|
||||
color: #e5e7eb;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wrapper-item-condition select {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
color: #e5e7eb;
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.wrapper-item-condition select:focus {
|
||||
outline: none;
|
||||
border-color: rgba(147, 51, 234, 0.5);
|
||||
}
|
||||
|
||||
.wrapper-item-delete {
|
||||
padding: 4px 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.wrapper-item-delete:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border-color: rgba(239, 68, 68, 0.4);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.wrapper-items-empty {
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
padding: 12px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.wrapper-condition-select {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
color: #e5e7eb;
|
||||
font-size: 0.82rem;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wrapper-condition-select:focus {
|
||||
outline: none;
|
||||
border-color: rgba(147, 51, 234, 0.5);
|
||||
}
|
||||
|
||||
.wrapper-preview-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.wrapper-preview-toggle:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.wrapper-preview-toggle i {
|
||||
transition: transform 0.2s;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.wrapper-preview-toggle i.expanded {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.wrapper-preview-content {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.5;
|
||||
padding: 12px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
white-space: pre;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.settings-hint {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
@@ -6460,3 +6610,85 @@ input[type="text"].uuid-input,
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Version Selection Styles */
|
||||
.version-list-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.version-list-container::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.version-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.version-item:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.version-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.version-name {
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.version-meta {
|
||||
font-size: 0.8rem;
|
||||
color: #a0a0a0;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.version-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.version-meta i {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.version-actions .btn-install {
|
||||
padding: 0.5rem 1.25rem;
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.version-actions .btn-install:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.3);
|
||||
background: linear-gradient(135deg, #4f93f6, #3b82f6);
|
||||
}
|
||||
|
||||
.loading-versions {
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -858,6 +858,212 @@ function checkLaunchReady() {
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// JAVA WRAPPER CONFIGURATION (Structured)
|
||||
// =============================================================================
|
||||
|
||||
const DEFAULT_WRAPPER_CONFIG = {
|
||||
stripFlags: ['-XX:+UseCompactObjectHeaders'],
|
||||
injectArgs: [
|
||||
{ arg: '--disable-sentry', condition: 'server' }
|
||||
]
|
||||
};
|
||||
|
||||
function getDefaultWrapperConfig() {
|
||||
return JSON.parse(JSON.stringify(DEFAULT_WRAPPER_CONFIG));
|
||||
}
|
||||
|
||||
function loadWrapperConfig() {
|
||||
const config = loadConfig();
|
||||
if (config.javaWrapperConfig && typeof config.javaWrapperConfig === 'object') {
|
||||
const wc = config.javaWrapperConfig;
|
||||
if (Array.isArray(wc.stripFlags) && Array.isArray(wc.injectArgs)) {
|
||||
const loaded = JSON.parse(JSON.stringify(wc));
|
||||
// Normalize entries: ensure every injectArg has a valid condition
|
||||
for (const entry of loaded.injectArgs) {
|
||||
if (!['server', 'always'].includes(entry.condition)) {
|
||||
entry.condition = 'always';
|
||||
}
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
}
|
||||
return getDefaultWrapperConfig();
|
||||
}
|
||||
|
||||
function saveWrapperConfig(wrapperConfig) {
|
||||
if (!wrapperConfig || typeof wrapperConfig !== 'object') {
|
||||
throw new Error('Invalid wrapper config');
|
||||
}
|
||||
if (!Array.isArray(wrapperConfig.stripFlags) || !Array.isArray(wrapperConfig.injectArgs)) {
|
||||
throw new Error('Invalid wrapper config structure');
|
||||
}
|
||||
// Validate injectArgs entries
|
||||
for (const entry of wrapperConfig.injectArgs) {
|
||||
if (!entry.arg || typeof entry.arg !== 'string') {
|
||||
throw new Error('Each inject arg must have a string "arg" property');
|
||||
}
|
||||
if (!['server', 'always'].includes(entry.condition)) {
|
||||
throw new Error('Inject arg condition must be "server" or "always"');
|
||||
}
|
||||
}
|
||||
saveConfig({ javaWrapperConfig: wrapperConfig });
|
||||
console.log('[Config] Wrapper config saved');
|
||||
}
|
||||
|
||||
function resetWrapperConfig() {
|
||||
const config = loadConfig();
|
||||
delete config.javaWrapperConfig;
|
||||
delete config.javaWrapperScripts; // Clean up legacy key if present
|
||||
|
||||
// Write the cleaned config using the same atomic pattern as saveConfig.
|
||||
// We cannot use saveConfig() here because it merges (spread) which cannot remove keys.
|
||||
const data = JSON.stringify(config, null, 2);
|
||||
fs.writeFileSync(CONFIG_TEMP, data, 'utf8');
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
fs.copyFileSync(CONFIG_FILE, CONFIG_BACKUP);
|
||||
}
|
||||
fs.renameSync(CONFIG_TEMP, CONFIG_FILE);
|
||||
console.log('[Config] Wrapper config reset to default');
|
||||
return getDefaultWrapperConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a platform-specific wrapper script from structured config
|
||||
* @param {Object} config - { stripFlags: string[], injectArgs: {arg, condition}[] }
|
||||
* @param {string} platform - 'darwin', 'win32', or 'linux'
|
||||
* @param {string|null} javaBin - Path to real java binary (required for darwin/linux)
|
||||
* @returns {string} Generated script content
|
||||
*/
|
||||
function generateWrapperScript(config, platform, javaBin) {
|
||||
const { stripFlags, injectArgs } = config;
|
||||
const alwaysArgs = injectArgs.filter(a => a.condition === 'always');
|
||||
const serverArgs = injectArgs.filter(a => a.condition === 'server');
|
||||
|
||||
if (platform === 'win32') {
|
||||
return _generateWindowsWrapper(stripFlags, alwaysArgs, serverArgs);
|
||||
} else {
|
||||
return _generateUnixWrapper(stripFlags, alwaysArgs, serverArgs, javaBin);
|
||||
}
|
||||
}
|
||||
|
||||
function _generateUnixWrapper(stripFlags, alwaysArgs, serverArgs, javaBin) {
|
||||
const lines = [
|
||||
'#!/bin/bash',
|
||||
'# Java wrapper - generated by HytaleF2P launcher',
|
||||
`REAL_JAVA="${javaBin || '${JAVA_BIN}'}"`,
|
||||
'ARGS=("$@")',
|
||||
''
|
||||
];
|
||||
|
||||
// Strip flags
|
||||
if (stripFlags.length > 0) {
|
||||
lines.push('# Strip JVM flags');
|
||||
lines.push('FILTERED_ARGS=()');
|
||||
lines.push('for arg in "${ARGS[@]}"; do');
|
||||
lines.push(' case "$arg" in');
|
||||
for (const flag of stripFlags) {
|
||||
lines.push(` "${flag}") echo "[Wrapper] Stripped: $arg" ;;`);
|
||||
}
|
||||
lines.push(' *) FILTERED_ARGS+=("$arg") ;;');
|
||||
lines.push(' esac');
|
||||
lines.push('done');
|
||||
} else {
|
||||
lines.push('FILTERED_ARGS=("${ARGS[@]}")');
|
||||
}
|
||||
lines.push('');
|
||||
|
||||
// Always-inject args
|
||||
if (alwaysArgs.length > 0) {
|
||||
lines.push('# Inject args (always)');
|
||||
for (const a of alwaysArgs) {
|
||||
lines.push(`FILTERED_ARGS+=("${a.arg}")`);
|
||||
lines.push(`echo "[Wrapper] Injected ${a.arg}"`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Server-conditional args (appended after HytaleServer.jar if present)
|
||||
if (serverArgs.length > 0) {
|
||||
lines.push('# Inject args (server only)');
|
||||
lines.push('IS_SERVER=false');
|
||||
lines.push('for arg in "${FILTERED_ARGS[@]}"; do');
|
||||
lines.push(' if [[ "$arg" == *"HytaleServer.jar"* ]]; then');
|
||||
lines.push(' IS_SERVER=true');
|
||||
lines.push(' break');
|
||||
lines.push(' fi');
|
||||
lines.push('done');
|
||||
lines.push('if [ "$IS_SERVER" = true ]; then');
|
||||
for (const a of serverArgs) {
|
||||
lines.push(` FILTERED_ARGS+=("${a.arg}")`);
|
||||
lines.push(` echo "[Wrapper] Injected ${a.arg}"`);
|
||||
}
|
||||
lines.push('fi');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('echo "[Wrapper] Executing: $REAL_JAVA ${FILTERED_ARGS[*]}"');
|
||||
lines.push('exec "$REAL_JAVA" "${FILTERED_ARGS[@]}"');
|
||||
lines.push('');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function _generateWindowsWrapper(stripFlags, alwaysArgs, serverArgs) {
|
||||
const lines = [
|
||||
'@echo off',
|
||||
'setlocal EnableDelayedExpansion',
|
||||
'',
|
||||
'REM Java wrapper - generated by HytaleF2P launcher',
|
||||
'set "REAL_JAVA=%~dp0java-original.exe"',
|
||||
'set "ARGS=%*"',
|
||||
''
|
||||
];
|
||||
|
||||
// Strip flags using string replacement
|
||||
if (stripFlags.length > 0) {
|
||||
lines.push('REM Strip JVM flags');
|
||||
for (const flag of stripFlags) {
|
||||
lines.push(`set "ARGS=!ARGS:${flag}=!"`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Always-inject args
|
||||
const alwaysExtra = alwaysArgs.map(a => a.arg).join(' ');
|
||||
|
||||
// Server-conditional args
|
||||
if (serverArgs.length > 0) {
|
||||
const serverExtra = serverArgs.map(a => a.arg).join(' ');
|
||||
lines.push('REM Check if running HytaleServer.jar and inject server args');
|
||||
lines.push('echo !ARGS! | findstr /i "HytaleServer.jar" >nul 2>&1');
|
||||
lines.push('if "!ERRORLEVEL!"=="0" (');
|
||||
if (alwaysExtra) {
|
||||
lines.push(` echo [Wrapper] Injected ${alwaysExtra} ${serverExtra}`);
|
||||
lines.push(` "%REAL_JAVA%" !ARGS! ${alwaysExtra} ${serverExtra}`);
|
||||
} else {
|
||||
lines.push(` echo [Wrapper] Injected ${serverExtra}`);
|
||||
lines.push(` "%REAL_JAVA%" !ARGS! ${serverExtra}`);
|
||||
}
|
||||
lines.push(') else (');
|
||||
if (alwaysExtra) {
|
||||
lines.push(` "%REAL_JAVA%" !ARGS! ${alwaysExtra}`);
|
||||
} else {
|
||||
lines.push(' "%REAL_JAVA%" !ARGS!');
|
||||
}
|
||||
lines.push(')');
|
||||
} else if (alwaysExtra) {
|
||||
lines.push(`"%REAL_JAVA%" !ARGS! ${alwaysExtra}`);
|
||||
} else {
|
||||
lines.push('"%REAL_JAVA%" !ARGS!');
|
||||
}
|
||||
|
||||
lines.push('exit /b !ERRORLEVEL!');
|
||||
lines.push('');
|
||||
|
||||
return lines.join('\r\n');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
@@ -924,6 +1130,13 @@ module.exports = {
|
||||
saveVersionBranch,
|
||||
loadVersionBranch,
|
||||
|
||||
// Java Wrapper Config
|
||||
getDefaultWrapperConfig,
|
||||
loadWrapperConfig,
|
||||
saveWrapperConfig,
|
||||
resetWrapperConfig,
|
||||
generateWrapperScript,
|
||||
|
||||
// Constants
|
||||
CONFIG_FILE,
|
||||
UUID_STORE_FILE
|
||||
|
||||
@@ -45,7 +45,13 @@ const {
|
||||
saveVersionClient,
|
||||
loadVersionClient,
|
||||
saveVersionBranch,
|
||||
loadVersionBranch
|
||||
loadVersionBranch,
|
||||
// Java Wrapper Config
|
||||
getDefaultWrapperConfig,
|
||||
loadWrapperConfig,
|
||||
saveWrapperConfig,
|
||||
resetWrapperConfig,
|
||||
generateWrapperScript
|
||||
} = require('./core/config');
|
||||
|
||||
const { getResolvedAppDir, getModsPath } = require('./core/paths');
|
||||
@@ -78,7 +84,8 @@ const {
|
||||
loadInstalledMods,
|
||||
downloadMod,
|
||||
uninstallMod,
|
||||
toggleMod
|
||||
toggleMod,
|
||||
getModFiles
|
||||
} = require('./managers/modManager');
|
||||
|
||||
// Services
|
||||
@@ -181,6 +188,7 @@ module.exports = {
|
||||
downloadMod,
|
||||
uninstallMod,
|
||||
toggleMod,
|
||||
getModFiles,
|
||||
saveModsToConfig,
|
||||
loadModsFromConfig,
|
||||
|
||||
@@ -197,6 +205,13 @@ module.exports = {
|
||||
proposeGameUpdate,
|
||||
handleFirstLaunchCheck,
|
||||
|
||||
// Java Wrapper Config functions
|
||||
getDefaultWrapperConfig,
|
||||
loadWrapperConfig,
|
||||
saveWrapperConfig,
|
||||
resetWrapperConfig,
|
||||
generateWrapperScript,
|
||||
|
||||
// Path functions
|
||||
getResolvedAppDir
|
||||
};
|
||||
|
||||
@@ -18,7 +18,9 @@ const {
|
||||
saveVersionClient,
|
||||
loadUsername,
|
||||
hasUsername,
|
||||
checkLaunchReady
|
||||
checkLaunchReady,
|
||||
loadWrapperConfig,
|
||||
generateWrapperScript
|
||||
} = require('../core/config');
|
||||
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
|
||||
const { getLatestClientVersion } = require('../services/versionManager');
|
||||
@@ -27,6 +29,7 @@ const { ensureGameInstalled } = require('./differentialUpdateManager');
|
||||
const { syncModsForCurrentProfile } = require('./modManager');
|
||||
const { getUserDataPath } = require('../utils/userDataMigration');
|
||||
const { syncServerList } = require('../utils/serverListSync');
|
||||
const { killGameProcesses } = require('./gameManager');
|
||||
|
||||
// Client patcher for custom auth server (sanasol.ws)
|
||||
let clientPatcher = null;
|
||||
@@ -327,23 +330,13 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
|
||||
console.log('Signed server binaries (after patching)');
|
||||
}
|
||||
|
||||
// Create java wrapper (must be signed on macOS)
|
||||
if (javaBin && fs.existsSync(javaBin)) {
|
||||
const javaWrapperPath = path.join(path.dirname(javaBin), 'java-wrapper');
|
||||
const wrapperScript = `#!/bin/bash
|
||||
# Java wrapper for macOS - adds --disable-sentry to fix Sentry hang issue
|
||||
REAL_JAVA="${javaBin}"
|
||||
ARGS=("$@")
|
||||
for i in "\${!ARGS[@]}"; do
|
||||
if [[ "\${ARGS[$i]}" == *"HytaleServer.jar"* ]]; then
|
||||
ARGS=("\${ARGS[@]:0:$((i+1))}" "--disable-sentry" "\${ARGS[@]:$((i+1))}")
|
||||
break
|
||||
fi
|
||||
done
|
||||
exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
`;
|
||||
const wrapperScript = generateWrapperScript(loadWrapperConfig(), 'darwin', javaBin);
|
||||
fs.writeFileSync(javaWrapperPath, wrapperScript, { mode: 0o755 });
|
||||
await signPath(javaWrapperPath, false);
|
||||
console.log('Created java wrapper with --disable-sentry fix');
|
||||
console.log('Created java wrapper from config template');
|
||||
javaBin = javaWrapperPath;
|
||||
}
|
||||
} catch (signError) {
|
||||
@@ -352,6 +345,40 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
}
|
||||
}
|
||||
|
||||
// Windows: Create java wrapper to strip/inject JVM flags per wrapper config
|
||||
if (process.platform === 'win32' && javaBin && fs.existsSync(javaBin)) {
|
||||
try {
|
||||
const javaDir = path.dirname(javaBin);
|
||||
const javaOriginal = path.join(javaDir, 'java-original.exe');
|
||||
const javaWrapperPath = path.join(javaDir, 'java-wrapper.bat');
|
||||
|
||||
if (!fs.existsSync(javaOriginal)) {
|
||||
fs.copyFileSync(javaBin, javaOriginal);
|
||||
console.log('Backed up java.exe as java-original.exe');
|
||||
}
|
||||
|
||||
const wrapperScript = generateWrapperScript(loadWrapperConfig(), 'win32', null);
|
||||
fs.writeFileSync(javaWrapperPath, wrapperScript);
|
||||
console.log('Created Windows java wrapper from config template');
|
||||
javaBin = javaWrapperPath;
|
||||
} catch (wrapperError) {
|
||||
console.log('Notice: Windows java wrapper creation failed:', wrapperError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Linux: Create java wrapper to strip/inject JVM flags per wrapper config
|
||||
if (process.platform === 'linux' && javaBin && fs.existsSync(javaBin)) {
|
||||
try {
|
||||
const javaWrapperPath = path.join(path.dirname(javaBin), 'java-wrapper');
|
||||
const wrapperScript = generateWrapperScript(loadWrapperConfig(), 'linux', javaBin);
|
||||
fs.writeFileSync(javaWrapperPath, wrapperScript, { mode: 0o755 });
|
||||
console.log('Created Linux java wrapper from config template');
|
||||
javaBin = javaWrapperPath;
|
||||
} catch (wrapperError) {
|
||||
console.log('Notice: Linux java wrapper creation failed:', wrapperError.message);
|
||||
}
|
||||
}
|
||||
|
||||
const args = [
|
||||
'--app-dir', gameLatest,
|
||||
'--java-exec', javaBin,
|
||||
@@ -436,6 +463,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
|
||||
// This enables runtime auth patching without modifying the server 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
|
||||
async function safeRemoveDirectory(dirPath, maxRetries = 3) {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
@@ -50,8 +85,13 @@ async function safeRemoveDirectory(dirPath, maxRetries = 3) {
|
||||
return; // Success, exit the loop
|
||||
} catch (error) {
|
||||
console.warn(`Attempt ${attempt}/${maxRetries} failed to remove ${dirPath}: ${error.message}`);
|
||||
|
||||
|
||||
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)
|
||||
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
|
||||
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);
|
||||
}
|
||||
|
||||
// Check if game is running before attempting to delete files
|
||||
// Kill stalled game processes before attempting to delete files
|
||||
const gameRunning = await isGameRunning();
|
||||
if (gameRunning) {
|
||||
console.warn('[RepairGame] Game appears to be running. This may cause permission errors during repair.');
|
||||
console.log('[RepairGame] Please close the game before repairing, or wait for the repair to complete.');
|
||||
console.warn('[RepairGame] Game processes detected. Force-killing to release file locks...');
|
||||
if (progressCallback) {
|
||||
progressCallback('Stopping stalled game processes...', 20, null, null, null);
|
||||
}
|
||||
await killGameProcesses();
|
||||
}
|
||||
|
||||
// Delete Game and Cache Directory with retry logic
|
||||
@@ -964,5 +1007,6 @@ module.exports = {
|
||||
installGame,
|
||||
uninstallGame,
|
||||
checkExistingGameInstallation,
|
||||
repairGame
|
||||
repairGame,
|
||||
killGameProcesses
|
||||
};
|
||||
|
||||
@@ -285,6 +285,27 @@ async function toggleMod(modId, modsPath) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function getModFiles(modId) {
|
||||
try {
|
||||
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modId}/files`, {
|
||||
headers: {
|
||||
'x-api-key': API_KEY,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
params: {
|
||||
pageSize: 20,
|
||||
sortOrder: 'desc'
|
||||
}
|
||||
});
|
||||
|
||||
return response.data.data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching mod files:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function syncModsForCurrentProfile() {
|
||||
try {
|
||||
const activeProfile = profileManager.getActiveProfile();
|
||||
@@ -455,5 +476,6 @@ module.exports = {
|
||||
syncModsForCurrentProfile,
|
||||
generateModId,
|
||||
extractModName,
|
||||
extractVersion
|
||||
};
|
||||
extractVersion,
|
||||
getModFiles
|
||||
};
|
||||
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 isIntel = lowerLine.includes('intel') || vendorId === '8086';
|
||||
|
||||
// Intel Arc detection
|
||||
const isIntelArc = isIntel && (lowerName.includes('arc') || lowerName.includes('a770') || lowerName.includes('a750') || lowerName.includes('a380'));
|
||||
// Intel Arc discrete GPU detection
|
||||
// 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';
|
||||
if (isNvidia) vendor = 'nvidia';
|
||||
|
||||
130
main.js
130
main.js
@@ -1068,7 +1068,7 @@ ipcMain.handle('load-settings', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher');
|
||||
const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getModFiles, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher');
|
||||
const os = require('os');
|
||||
|
||||
ipcMain.handle('get-local-app-data', async () => {
|
||||
@@ -1118,6 +1118,15 @@ ipcMain.handle('download-mod', async (event, modInfo) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-mod-files', async (event, modId) => {
|
||||
try {
|
||||
return await getModFiles(modId);
|
||||
} catch (error) {
|
||||
console.error('Error getting mod files:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('uninstall-mod', async (event, modId, modsPath) => {
|
||||
try {
|
||||
return await uninstallMod(modId, modsPath);
|
||||
@@ -1407,6 +1416,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 () => {
|
||||
try {
|
||||
const logDir = logger.getLogDirectory();
|
||||
@@ -1462,3 +1548,45 @@ ipcMain.handle('profile-update', async (event, id, updates) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Java Wrapper Config IPC
|
||||
ipcMain.handle('load-wrapper-config', () => {
|
||||
const { loadWrapperConfig } = require('./backend/launcher');
|
||||
return loadWrapperConfig();
|
||||
});
|
||||
|
||||
ipcMain.handle('save-wrapper-config', (event, config) => {
|
||||
try {
|
||||
const { saveWrapperConfig } = require('./backend/launcher');
|
||||
saveWrapperConfig(config);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error saving wrapper config:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('reset-wrapper-config', () => {
|
||||
try {
|
||||
const { resetWrapperConfig } = require('./backend/launcher');
|
||||
const config = resetWrapperConfig();
|
||||
return { success: true, config };
|
||||
} catch (error) {
|
||||
console.error('Error resetting wrapper config:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-default-wrapper-config', () => {
|
||||
const { getDefaultWrapperConfig } = require('./backend/launcher');
|
||||
return getDefaultWrapperConfig();
|
||||
});
|
||||
|
||||
ipcMain.handle('preview-wrapper-script', (event, config, platform) => {
|
||||
const { generateWrapperScript } = require('./backend/launcher');
|
||||
return generateWrapperScript(config || require('./backend/launcher').loadWrapperConfig(), platform || process.platform, '/path/to/java');
|
||||
});
|
||||
|
||||
ipcMain.handle('get-current-platform', () => {
|
||||
return process.platform;
|
||||
});
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hytale-f2p-launcher",
|
||||
"version": "2.2.0",
|
||||
"version": "2.4.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hytale-f2p-launcher",
|
||||
"version": "2.2.0",
|
||||
"version": "2.4.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hytale-f2p-launcher",
|
||||
"version": "2.3.8",
|
||||
"version": "2.4.2",
|
||||
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
||||
"homepage": "https://git.sanhost.net/sanasol/hytale-f2p",
|
||||
"main": "main.js",
|
||||
@@ -26,7 +26,6 @@
|
||||
"electron",
|
||||
"auto-update",
|
||||
"mod-manager"
|
||||
|
||||
],
|
||||
"maintainers": [
|
||||
{
|
||||
|
||||
10
preload.js
10
preload.js
@@ -45,6 +45,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
loadInstalledMods: (modsPath) => ipcRenderer.invoke('load-installed-mods', modsPath),
|
||||
downloadMod: (modInfo) => ipcRenderer.invoke('download-mod', modInfo),
|
||||
uninstallMod: (modId, modsPath) => ipcRenderer.invoke('uninstall-mod', modId, modsPath),
|
||||
getModFiles: (modId) => ipcRenderer.invoke('get-mod-files', modId),
|
||||
toggleMod: (modId, modsPath) => ipcRenderer.invoke('toggle-mod', modId, modsPath),
|
||||
selectModFiles: () => ipcRenderer.invoke('select-mod-files'),
|
||||
copyModFile: (sourcePath, modsPath) => ipcRenderer.invoke('copy-mod-file', sourcePath, modsPath),
|
||||
@@ -94,6 +95,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
|
||||
openLogsFolder: () => ipcRenderer.invoke('open-logs-folder'),
|
||||
getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines),
|
||||
sendLogs: () => ipcRenderer.invoke('send-logs'),
|
||||
|
||||
// UUID Management methods
|
||||
getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'),
|
||||
@@ -103,6 +105,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
deleteUuidForUser: (username) => ipcRenderer.invoke('delete-uuid-for-user', username),
|
||||
resetCurrentUserUuid: () => ipcRenderer.invoke('reset-current-user-uuid'),
|
||||
|
||||
// Java Wrapper Config API
|
||||
loadWrapperConfig: () => ipcRenderer.invoke('load-wrapper-config'),
|
||||
saveWrapperConfig: (config) => ipcRenderer.invoke('save-wrapper-config', config),
|
||||
resetWrapperConfig: () => ipcRenderer.invoke('reset-wrapper-config'),
|
||||
getDefaultWrapperConfig: () => ipcRenderer.invoke('get-default-wrapper-config'),
|
||||
previewWrapperScript: (config, platform) => ipcRenderer.invoke('preview-wrapper-script', config, platform),
|
||||
getCurrentPlatform: () => ipcRenderer.invoke('get-current-platform'),
|
||||
|
||||
// Profile API
|
||||
profile: {
|
||||
create: (name) => ipcRenderer.invoke('profile-create', name),
|
||||
|
||||
@@ -15,17 +15,32 @@ Host your own Hytale server. The scripts handle everything automatically.
|
||||
|
||||
## 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
|
||||
2. Double-click `start.bat`
|
||||
3. Done — server starts on port **5520**
|
||||
|
||||
### Linux / macOS
|
||||
### Linux / macOS (self-hosted)
|
||||
|
||||
```bash
|
||||
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
|
||||
./start.sh
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user