This commit is contained in:
Fazri Gading
2026-01-21 00:31:20 +08:00
11 changed files with 413 additions and 23 deletions

View File

@@ -365,6 +365,41 @@
</button>
</div>
</div>
<div class="settings-input-group">
<label class="settings-input-label">GPU Preference</label>
<div class="segmented-control">
<input type="radio" id="gpu-auto" name="gpuPreference" value="auto" checked>
<label for="gpu-auto">Auto</label>
<input type="radio" id="gpu-integrated" name="gpuPreference" value="integrated">
<label for="gpu-integrated">Integrated</label>
<input type="radio" id="gpu-dedicated" name="gpuPreference" value="dedicated">
<label for="gpu-dedicated">Dedicated</label>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
Select your preferred GPU (Linux: affects DRI_PRIME)
</p>
<div id="gpu-detection-info" class="gpu-detection-info"></div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fab fa-discord"></i>
Discord Integration
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="discordRPCCheck" checked />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Enable Discord Rich Presence</div>
<div class="checkbox-description">Show your launcher activity on Discord</div>
</div>
</label>
</div>
</div>
<div class="settings-section">
@@ -406,6 +441,48 @@
</div>
</div>
</div>
<div class="settings-section">
<h3 class="settings-section-title">
<i class="fas fa-coffee"></i>
Java Runtime
</h3>
<div class="settings-option">
<label class="settings-checkbox">
<input type="checkbox" id="customJavaCheck" />
<span class="checkmark"></span>
<div class="checkbox-content">
<div class="checkbox-title">Use Custom Java Path</div>
<div class="checkbox-description">Override the bundled Java runtime with your own installation</div>
</div>
</label>
</div>
<div id="customJavaOptions" class="custom-java-options" style="display: none;">
<div class="settings-input-group">
<label class="settings-input-label">Java Executable Path</label>
<div class="settings-input-with-button">
<input
type="text"
id="customJavaPath"
class="settings-input"
placeholder="Select Java path..."
readonly
/>
<button id="browseJavaBtn" class="settings-browse-btn">
<i class="fas fa-folder-open"></i>
Browse
</button>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
Select the Java installation folder (supports Windows, Mac, Linux)
</p>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -211,6 +211,15 @@ export async function launch() {
if (window.SettingsAPI && window.SettingsAPI.getCurrentJavaPath) {
javaPath = window.SettingsAPI.getCurrentJavaPath();
}
let gpuPreference = 'auto';
try {
if (window.electronAPI && window.electronAPI.loadGpuPreference) {
gpuPreference = await window.electronAPI.loadGpuPreference();
}
} catch (error) {
console.error('Error loading GPU preference:', error);
}
if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true;
@@ -223,8 +232,8 @@ export async function launch() {
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Starting game...' });
if (window.electronAPI && window.electronAPI.launchGame) {
const result = await window.electronAPI.launchGame(playerName, javaPath, '');
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
isDownloading = false;
if (window.LauncherUI) {

View File

@@ -5,6 +5,7 @@ let customJavaPath;
let browseJavaBtn;
let settingsPlayerName;
let discordRPCCheck;
let gpuPreferenceRadios;
// UUID Management elements
let currentUuidDisplay;
@@ -142,6 +143,7 @@ function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCance
document.addEventListener('keydown', handleEscape);
}
export function initSettings() {
setupSettingsElements();
loadAllSettings();
@@ -154,6 +156,7 @@ function setupSettingsElements() {
browseJavaBtn = document.getElementById('browseJavaBtn');
settingsPlayerName = document.getElementById('settingsPlayerName');
discordRPCCheck = document.getElementById('discordRPCCheck');
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
// UUID Management elements
currentUuidDisplay = document.getElementById('currentUuid');
@@ -226,6 +229,15 @@ function setupSettingsElements() {
}
});
}
if (gpuPreferenceRadios) {
gpuPreferenceRadios.forEach(radio => {
radio.addEventListener('change', async () => {
await saveGpuPreference();
await updateGpuLabel();
});
});
}
}
function toggleCustomJava() {
@@ -361,11 +373,85 @@ async function loadPlayerName() {
}
}
async function saveGpuPreference() {
try {
if (window.electronAPI && window.electronAPI.saveGpuPreference && gpuPreferenceRadios) {
const gpuPreference = Array.from(gpuPreferenceRadios).find(radio => radio.checked)?.value || 'auto';
await window.electronAPI.saveGpuPreference(gpuPreference);
}
} catch (error) {
console.error('Error saving GPU preference:', error);
}
}
async function updateGpuLabel() {
const detectionInfo = document.getElementById('gpu-detection-info');
if (!detectionInfo) return;
if (gpuPreferenceRadios) {
const checked = Array.from(gpuPreferenceRadios).find(radio => radio.checked);
if (checked) {
try {
if (window.electronAPI && window.electronAPI.getDetectedGpu) {
const detected = await window.electronAPI.getDetectedGpu();
if (checked.value === 'auto') {
if (detected.dedicatedName) {
detectionInfo.textContent = `dGPU detected, using ${detected.dedicatedName}`;
} else {
detectionInfo.textContent = `dGPU not detected, using iGPU (${detected.integratedName}) instead`;
}
detectionInfo.style.display = 'block';
} else if (checked.value === 'integrated') {
detectionInfo.textContent = `Detected: ${detected.integratedName}`;
detectionInfo.style.display = 'block';
} else if (checked.value === 'dedicated') {
if (detected.dedicatedName) {
detectionInfo.textContent = `Detected: ${detected.dedicatedName}`;
} else {
detectionInfo.textContent = `No dedicated GPU detected`;
}
detectionInfo.style.display = 'block';
} else {
detectionInfo.style.display = 'none';
}
}
} catch (error) {
console.error('Error getting detected GPU:', error);
detectionInfo.style.display = 'none';
}
} else {
detectionInfo.style.display = 'none';
}
} else {
detectionInfo.style.display = 'none';
}
}
async function loadGpuPreference() {
try {
if (window.electronAPI && window.electronAPI.loadGpuPreference && gpuPreferenceRadios) {
const savedPreference = await window.electronAPI.loadGpuPreference();
if (savedPreference) {
for (const radio of gpuPreferenceRadios) {
if (radio.value === savedPreference) {
radio.checked = true;
break;
}
}
await updateGpuLabel();
}
}
} catch (error) {
console.error('Error loading GPU preference:', error);
}
}
async function loadAllSettings() {
await loadCustomJavaPath();
await loadPlayerName();
await loadCurrentUuid();
await loadDiscordRPC();
await loadGpuPreference();
}
async function openGameLocation() {

View File

@@ -1005,6 +1005,7 @@ body {
transform: translateY(0.5rem);
transition: all 0.3s ease 0.1s;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -1888,6 +1889,7 @@ body {
line-height: 1.5;
margin: 0.75rem 0;
display: -webkit-box;
line-clamp: 3;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -2675,6 +2677,7 @@ body {
color: rgba(255, 255, 255, 0.5);
line-height: 1.4;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -3095,6 +3098,7 @@ body {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-clamp: 2;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
@@ -3758,7 +3762,7 @@ body {
}
.settings-header {
margin-bottom: 3rem;
margin-bottom: 1rem;
text-align: center;
}
@@ -4047,6 +4051,14 @@ body {
font-size: 0.75rem;
}
.gpu-detection-info {
margin-top: 0.5rem;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.7);
font-weight: 500;
display: none;
}
#settings-page {
opacity: 0;
@@ -4078,7 +4090,53 @@ body {
background: linear-gradient(135deg, #a855f7, #60a5fa);
}
.segmented-control {
display: flex;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
backdrop-filter: blur(10px);
overflow: hidden;
transition: all 0.3s ease;
}
.segmented-control input[type="radio"] {
display: none;
}
.segmented-control label {
flex: 1;
padding: 0.75rem 1rem;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
color: #d1d5db;
font-size: 0.875rem;
background: transparent;
border-right: 1px solid rgba(255, 255, 255, 0.1);
font-family: 'JetBrains Mono', monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 500;
}
.segmented-control label:last-child {
border-right: none;
}
.segmented-control input[type="radio"]:checked + label {
background: linear-gradient(135deg, #9333ea, #3b82f6);
color: white;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.segmented-control label:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.segmented-control input[type="radio"]:checked + label:hover {
background: linear-gradient(135deg, #7c3aed, #2563eb);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.segmented-control:focus-within {
border-color: rgba(147, 51, 234, 0.3);
box-shadow: 0 0 20px rgba(147, 51, 234, 0.1);
}
#update-popup-overlay {

View File

@@ -277,6 +277,15 @@ function loadChatColor() {
return config.chatColor || '#3498db';
}
function saveGpuPreference(gpuPreference) {
saveConfig({ gpuPreference: gpuPreference || 'auto' });
}
function loadGpuPreference() {
const config = loadConfig();
return config.gpuPreference || 'auto';
}
module.exports = {
loadConfig,
saveConfig,
@@ -308,5 +317,8 @@ module.exports = {
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
resetCurrentUserUuid
resetCurrentUserUuid,
// GPU Preference exports
saveGpuPreference,
loadGpuPreference
};

View File

@@ -20,14 +20,16 @@ const {
getUuidForUser,
isFirstLaunch,
markAsLaunched,
CONFIG_FILE,
// UUID Management
getCurrentUuid,
getAllUuidMappings,
setUuidForUser,
generateNewUuid,
deleteUuidForUser,
resetCurrentUserUuid
resetCurrentUserUuid,
// GPU Preference
saveGpuPreference,
loadGpuPreference
} = require('./core/config');
const { getResolvedAppDir, getModsPath } = require('./core/paths');
@@ -77,6 +79,9 @@ const {
handleFirstLaunchCheck
} = require('./services/firstLaunch');
// Utils
const { detectGpu } = require('./utils/platformUtils');
// Re-export all functions to maintain backward compatibility
module.exports = {
// Game launch functions
@@ -111,6 +116,11 @@ module.exports = {
saveDiscordRPC,
loadDiscordRPC,
// GPU Preference functions
saveGpuPreference,
loadGpuPreference,
detectGpu,
// Version functions
getInstalledClientVersion,
getLatestClientVersion,
@@ -150,7 +160,7 @@ module.exports = {
checkExistingGameInstallation,
proposeGameUpdate,
handleFirstLaunchCheck,
// Path functions
getResolvedAppDir
};

View File

@@ -6,7 +6,7 @@ const { promisify } = require('util');
const { spawn } = require('child_process');
const { v4: uuidv4 } = require('uuid');
const { getResolvedAppDir, findClientPath } = require('../core/paths');
const { setupWaylandEnvironment } = require('../utils/platformUtils');
const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platformUtils');
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain } = require('../core/config');
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager');
@@ -101,7 +101,7 @@ function generateLocalTokens(uuid, name) {
};
}
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto') {
const customAppDir = getResolvedAppDir(installPathOverride);
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest');
@@ -276,10 +276,13 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
console.log('Starting game...');
console.log(`Command: "${clientPath}" ${args.join(' ')}`);
const env = { ...process.env };
const env = { ...process.env };
const waylandEnv = setupWaylandEnvironment();
Object.assign(env, waylandEnv);
const waylandEnv = setupWaylandEnvironment();
Object.assign(env, waylandEnv);
const gpuEnv = setupGpuEnvironment(gpuPreference);
Object.assign(env, gpuEnv);
try {
let spawnOptions = {
@@ -352,7 +355,7 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
}
}
async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) {
async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto') {
try {
if (progressCallback) {
progressCallback('Checking for updates...', 0, null, null, null);
@@ -403,7 +406,7 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
progressCallback('Launching game...', 80, null, null, null);
}
return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride);
return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference);
} catch (error) {
console.error('Error in version check and launch:', error);
if (progressCallback) {

View File

@@ -65,9 +65,100 @@ function setupWaylandEnvironment() {
return envVars;
}
function detectGpu() {
if (process.platform !== 'linux') {
return { mode: 'integrated', vendor: 'intel', integratedName: 'Unknown', dedicatedName: null };
}
try {
const output = execSync('lspci -nn | grep \'VGA\\|3D\'', { encoding: 'utf8' });
// console.log('GPU detection raw output:', output);
const lines = output.split('\n').filter(line => line.trim());
// console.log('GPU detection parsed lines:', lines);
let integratedName = null;
let dedicatedName = null;
let hasNvidia = false;
let hasAmd = false;
for (const line of lines) {
// console.log('Checking line:', line);
if (line.includes('VGA') || line.includes('3D')) {
// console.log('Line contains VGA or 3D');
const match = line.match(/\[([^\]]+)\]/g);
let modelName = null;
if (match && match.length >= 2) {
modelName = match[1].slice(1, -1);
}
if (line.includes('10de:') || line.toLowerCase().includes('nvidia')) {
hasNvidia = true;
dedicatedName = "NVIDIA " + modelName || 'NVIDIA GPU';
console.log('Detected NVIDIA GPU:', dedicatedName);
} else if (line.includes('1002:') || line.toLowerCase().includes('amd') || line.toLowerCase().includes('radeon')) {
hasAmd = true;
dedicatedName = "AMD " + modelName || 'AMD GPU';
console.log('Detected AMD GPU:', dedicatedName);
} else if (line.includes('8086:') || line.toLowerCase().includes('intel')) {
integratedName = "Intel " + modelName || 'Intel GPU';
console.log('Detected Intel GPU:', integratedName);
}
}
}
// console.log('hasNvidia:', hasNvidia, 'hasAmd:', hasAmd, 'integratedName:', integratedName, 'dedicatedName:', dedicatedName);
if (hasNvidia) {
return { mode: 'dedicated', vendor: 'nvidia', integratedName: integratedName || 'Intel GPU', dedicatedName };
} else if (hasAmd) {
return { mode: 'dedicated', vendor: 'amd', integratedName: integratedName || 'Intel GPU', dedicatedName };
} else {
return { mode: 'integrated', vendor: 'intel', integratedName: integratedName || 'Intel GPU', dedicatedName: null };
}
} catch (error) {
console.warn('GPU detection failed, falling back to integrated:', error.message);
return { mode: 'integrated', vendor: 'intel', integratedName: 'Unknown', dedicatedName: null };
}
}
function setupGpuEnvironment(gpuPreference) {
if (process.platform !== 'linux') {
return {};
}
let finalPreference = gpuPreference;
let detected = detectGpu();
if (gpuPreference === 'auto') {
finalPreference = detected.mode;
console.log(`Auto-detected GPU: ${detected.vendor} (${detected.mode})`);
}
console.log('Preferred GPU set to:', finalPreference);
const envVars = {};
if (finalPreference === 'dedicated') {
envVars.DRI_PRIME = '1';
if (detected.vendor === 'nvidia') {
envVars.__NV_PRIME_RENDER_OFFLOAD = '1';
envVars.__GLX_VENDOR_LIBRARY_NAME = 'nvidia';
envVars.__GL_SHADER_DISK_CACHE = '1';
envVars.__GL_SHADER_DISK_CACHE_PATH = '/tmp';
}
console.log('GPU environment variables:', envVars);
} else {
console.log('Using integrated GPU, no environment variables set');
}
return envVars;
}
module.exports = {
getOS,
getArch,
isWaylandSession,
setupWaylandEnvironment
setupWaylandEnvironment,
detectGpu,
setupGpuEnvironment
};

48
main.js
View File

@@ -161,6 +161,19 @@ app.whenReady().then(async () => {
console.log('Node.js version:', process.versions.node);
console.log('Log directory:', logger.getLogDirectory());
try {
const { loadGpuPreference, detectGpu } = require('./backend/launcher');
const savedPreference = loadGpuPreference();
if (savedPreference === 'auto') {
global.detectedGpu = detectGpu(); // if 'auto' selected = preload GPU detection
console.log('GPU auto-detection completed on startup:', global.detectedGpu);
} else {
console.log('GPU preference is manual, skipping auto-detection');
}
} catch (error) {
console.warn('Failed to preload GPU detection:', error.message);
global.detectedGpu = { mode: 'integrated', vendor: 'intel' };
}
// Initialize Profile Manager (runs migration if needed)
@@ -282,7 +295,7 @@ app.on('window-all-closed', () => {
}
});
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath) => {
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
try {
const progressCallback = (message, percent, speed, downloaded, total) => {
if (mainWindow && !mainWindow.isDestroyed()) {
@@ -297,8 +310,8 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath) =
}
};
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath);
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference);
return result;
} catch (error) {
console.error('Launch error:', error);
@@ -710,6 +723,35 @@ ipcMain.handle('get-update-info', async () => {
return updateManager.getUpdateInfo();
});
ipcMain.handle('get-gpu-info', () => {
try {
return app.getGPUInfo('complete');
} catch (error) {
console.error('Error getting GPU info:', error);
return {};
}
});
ipcMain.handle('save-gpu-preference', (event, gpuPreference) => {
const { saveGpuPreference } = require('./backend/launcher');
saveGpuPreference(gpuPreference);
return { success: true };
});
ipcMain.handle('load-gpu-preference', () => {
const { loadGpuPreference } = require('./backend/launcher');
return loadGpuPreference();
});
ipcMain.handle('get-detected-gpu', () => {
if (global.detectedGpu) {
return global.detectedGpu;
}
const { detectGpu } = require('./backend/launcher');
global.detectedGpu = detectGpu();
return global.detectedGpu;
});
ipcMain.handle('window-close', () => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.close();

View File

@@ -93,6 +93,3 @@
}
}
}

View File

@@ -1,7 +1,7 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
launchGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('launch-game', playerName, javaPath, installPath),
launchGame: (playerName, javaPath, installPath, gpuPreference) => ipcRenderer.invoke('launch-game', playerName, javaPath, installPath, gpuPreference),
installGame: (playerName, javaPath, installPath) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath),
closeWindow: () => ipcRenderer.invoke('window-close'),
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
@@ -48,6 +48,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
onUpdatePopup: (callback) => {
ipcRenderer.on('show-update-popup', (event, data) => callback(data));
},
getGpuInfo: () => ipcRenderer.invoke('get-gpu-info'),
saveGpuPreference: (gpuPreference) => ipcRenderer.invoke('save-gpu-preference', gpuPreference),
loadGpuPreference: () => ipcRenderer.invoke('load-gpu-preference'),
getDetectedGpu: () => ipcRenderer.invoke('get-detected-gpu'),
acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame),
markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'),