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),