diff --git a/GUI/index.html b/GUI/index.html index fe51bbc..312e4c6 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -430,8 +430,70 @@ + +
+

+ + Java Wrapper Configuration +

+

+ + Configure how the Java wrapper handles JVM flags and arguments at launch time. +

+ + + +
+
+ + +
+ + + +
+
+ + + +
+ + +
+ +
+ + +
+ + +
+
- +

diff --git a/GUI/js/settings.js b/GUI/js/settings.js index 3b33fee..cada7f0 100644 --- a/GUI/js/settings.js +++ b/GUI/js/settings.js @@ -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 = '
No flags configured
'; + return; + } + + container.innerHTML = ''; + _wrapperConfig.stripFlags.forEach((flag, index) => { + const item = document.createElement('div'); + item.className = 'wrapper-item'; + item.innerHTML = ` + ${escapeHtml(flag)} + + `; + 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 = '
No arguments configured
'; + 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 = ` + ${escapeHtml(entry.arg)} +
+ +
+ + `; + 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; + } +} diff --git a/GUI/locales/en.json b/GUI/locales/en.json index a58ccff..3387fdd 100644 --- a/GUI/locales/en.json +++ b/GUI/locales/en.json @@ -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...", diff --git a/GUI/style.css b/GUI/style.css index 2bb344e..566f7fe 100644 --- a/GUI/style.css +++ b/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; diff --git a/backend/core/config.js b/backend/core/config.js index 34b2fc3..e5c0db9 100644 --- a/backend/core/config.js +++ b/backend/core/config.js @@ -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 diff --git a/backend/launcher.js b/backend/launcher.js index 962323a..2f98005 100644 --- a/backend/launcher.js +++ b/backend/launcher.js @@ -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'); @@ -197,6 +203,13 @@ module.exports = { proposeGameUpdate, handleFirstLaunchCheck, + // Java Wrapper Config functions + getDefaultWrapperConfig, + loadWrapperConfig, + saveWrapperConfig, + resetWrapperConfig, + generateWrapperScript, + // Path functions getResolvedAppDir }; diff --git a/backend/managers/gameLauncher.js b/backend/managers/gameLauncher.js index 4786560..e3aa072 100644 --- a/backend/managers/gameLauncher.js +++ b/backend/managers/gameLauncher.js @@ -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, diff --git a/main.js b/main.js index a792a29..21fbcc8 100644 --- a/main.js +++ b/main.js @@ -1539,3 +1539,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; +}); + diff --git a/package.json b/package.json index a1c919b..e01a34a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/preload.js b/preload.js index 09e5438..f25dc2f 100644 --- a/preload.js +++ b/preload.js @@ -104,6 +104,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),