mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-26 14:51:48 -03:00
Compare commits
6 Commits
v2.4.0
...
5170f453ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5170f453ea | ||
|
|
db3b2fc966 | ||
|
|
2f5820e850 | ||
|
|
4abb455e0f | ||
|
|
d5828463f9 | ||
|
|
0d15659dc0 |
@@ -430,6 +430,68 @@
|
||||
</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">
|
||||
@@ -601,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>
|
||||
@@ -805,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>
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,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",
|
||||
@@ -221,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",
|
||||
@@ -239,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...",
|
||||
|
||||
216
GUI/style.css
216
GUI/style.css
@@ -4829,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;
|
||||
@@ -6476,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');
|
||||
@@ -328,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) {
|
||||
@@ -353,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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
53
main.js
53
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);
|
||||
@@ -1539,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;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hytale-f2p-launcher",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.1",
|
||||
"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",
|
||||
|
||||
@@ -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),
|
||||
@@ -104,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),
|
||||
|
||||
Reference in New Issue
Block a user