const { ipcRenderer, shell } = require('electron');
const fs = require('fs');
const path = require('path');
const https = require('https');
const extractZip = require('extract-zip');
const childProcess = require('child_process');
const DEFAULT_REPO = "smartcmd/MinecraftConsoles";
const DEFAULT_EXEC = "Minecraft.Client.exe";
const TARGET_FILE = "LCEWindows64.zip";
const LAUNCHER_REPO = "gradenGnostic/LegacyLauncher";
let releasesData = [];
let commitsData = [];
let currentReleaseIndex = 0;
let isProcessing = false;
let isGameRunning = false;
const Store = {
async get(key, defaultValue) {
const val = await ipcRenderer.invoke('store-get', key);
return val !== undefined ? val : defaultValue;
},
async set(key, value) {
return await ipcRenderer.invoke('store-set', key, value);
}
};
// Gamepad Controller Support
const GamepadManager = {
active: false,
lastInputTime: 0,
COOLDOWN: 180,
loopStarted: false,
lastAPressed: false,
init() {
window.addEventListener("gamepadconnected", () => {
if (!this.active) {
this.startLoop();
}
});
this.startLoop();
},
startLoop() {
if (this.loopStarted) return;
this.loopStarted = true;
const loop = () => {
try {
this.poll();
} catch (e) {
console.error("Gamepad poll error:", e);
}
requestAnimationFrame(loop);
};
loop();
},
poll() {
const gamepads = navigator.getGamepads();
let gp = null;
for (let i = 0; i < gamepads.length; i++) {
if (gamepads[i] && gamepads[i].connected && gamepads[i].buttons.length > 0) {
gp = gamepads[i];
break;
}
}
if (!gp) {
if (this.active) {
this.active = false;
showToast("Controller Disconnected");
}
return;
}
if (!this.active) {
this.active = true;
showToast("Controller Connected");
if (!document.activeElement || !document.activeElement.classList.contains('nav-item')) {
this.focusFirstVisible();
}
}
const now = Date.now();
const buttons = gp.buttons;
const axes = gp.axes;
// Helper to safely get button state
const isPressed = (idx) => buttons[idx] ? buttons[idx].pressed : false;
const getAxis = (idx) => axes[idx] !== undefined ? axes[idx] : 0;
if (now - this.lastInputTime > this.COOLDOWN) {
const threshold = 0.5;
const axisX = getAxis(0);
const axisY = getAxis(1);
// D-pad indices are usually 12, 13, 14, 15
const up = isPressed(12) || axisY < -threshold;
const down = isPressed(13) || axisY > threshold;
const left = isPressed(14) || axisX < -threshold;
const right = isPressed(15) || axisX > threshold;
if (up) { this.navigate('up'); this.lastInputTime = now; }
else if (down) { this.navigate('down'); this.lastInputTime = now; }
else if (left) { this.navigate('left'); this.lastInputTime = now; }
else if (right) { this.navigate('right'); this.lastInputTime = now; }
// Shoulder buttons (L1/R1 are 4/5)
else if (isPressed(4)) { this.cycleActiveSelection(-1); this.lastInputTime = now; }
else if (isPressed(5)) { this.cycleActiveSelection(1); this.lastInputTime = now; }
// B Button (Cancel)
else if (isPressed(1)) { this.cancelCurrent(); this.lastInputTime = now; }
// X Button (Refresh)
else if (isPressed(2)) { checkForUpdatesManual(); this.lastInputTime = now; }
}
// A Button (Confirm) - Immediate responsive check
const aPressed = isPressed(0);
if (aPressed && !this.lastAPressed) {
this.clickActive();
}
this.lastAPressed = aPressed;
// Right stick for scrolling (Axis 2/3 or 5)
const rStickY = getAxis(3) || getAxis(2) || getAxis(5);
if (Math.abs(rStickY) > 0.1) {
this.scrollActive(rStickY * 15);
}
},
focusFirstVisible() {
const visibleItems = this.getVisibleNavItems();
if (visibleItems.length > 0) visibleItems[0].focus();
},
getVisibleNavItems() {
// Find if any modal is open
const modals = ['update-modal', 'options-modal', 'profile-modal', 'servers-modal'];
let activeModal = null;
for (const id of modals) {
const m = document.getElementById(id);
if (m && m.style.display === 'flex') {
activeModal = m;
break;
}
}
const allItems = Array.from(document.querySelectorAll('.nav-item'));
return allItems.filter(item => {
if (activeModal) {
return activeModal.contains(item) && item.offsetParent !== null;
}
// Ensure item is not inside any hidden modal
let parent = item.parentElement;
while (parent) {
if (parent.classList?.contains('modal-overlay') && parent.style.display !== 'flex') return false;
parent = parent.parentElement;
}
return item.offsetParent !== null;
});
},
navigate(direction) {
const current = document.activeElement;
const items = this.getVisibleNavItems();
if (!items.includes(current)) {
items[0]?.focus();
return;
}
const currentRect = current.getBoundingClientRect();
const cx = currentRect.left + currentRect.width / 2;
const cy = currentRect.top + currentRect.height / 2;
let bestMatch = null;
let minScore = Infinity;
items.forEach(item => {
if (item === current) return;
const rect = item.getBoundingClientRect();
const ix = rect.left + rect.width / 2;
const iy = rect.top + rect.height / 2;
const dx = ix - cx;
const dy = iy - cy;
const angle = Math.atan2(dy, dx) * 180 / Math.PI;
let inDirection = false;
if (direction === 'right' && angle >= -45 && angle <= 45) inDirection = true;
if (direction === 'left' && (angle >= 135 || angle <= -135)) inDirection = true;
if (direction === 'down' && angle > 45 && angle < 135) inDirection = true;
if (direction === 'up' && angle < -45 && angle > -135) inDirection = true;
if (inDirection) {
const distance = Math.sqrt(dx * dx + dy * dy);
const penalty = (direction === 'left' || direction === 'right') ? Math.abs(dy) * 2.5 : Math.abs(dx) * 2.5;
const score = distance + penalty;
if (score < minScore) {
minScore = score;
bestMatch = item;
}
}
});
if (bestMatch) {
bestMatch.focus();
bestMatch.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
},
clickActive() {
const active = document.activeElement;
if (active && active.classList.contains('nav-item')) {
active.classList.add('active-bump');
setTimeout(() => active.classList.remove('active-bump'), 100);
if (active.tagName === 'INPUT' && active.type === 'checkbox') {
active.checked = !active.checked;
active.dispatchEvent(new Event('change'));
} else {
active.click();
}
}
},
cancelCurrent() {
const activeModal = this.getActiveModal();
if (activeModal) {
if (activeModal.id === 'options-modal') toggleOptions(false);
else if (activeModal.id === 'profile-modal') toggleProfile(false);
else if (activeModal.id === 'servers-modal') toggleServers(false);
else if (activeModal.id === 'update-modal') document.getElementById('btn-skip-update')?.click();
}
},
getActiveModal() {
const modals = ['update-modal', 'options-modal', 'profile-modal', 'servers-modal'];
for (const id of modals) {
const m = document.getElementById(id);
if (m && m.style.display === 'flex') return m;
}
return null;
},
cycleActiveSelection(dir) {
const active = document.activeElement;
if (active && active.id === 'version-select-box') {
const select = document.getElementById('version-select');
if (select) {
let newIdx = select.selectedIndex + dir;
if (newIdx < 0) newIdx = select.options.length - 1;
if (newIdx >= select.options.length) newIdx = 0;
select.selectedIndex = newIdx;
updateSelectedRelease();
}
} else if (active && active.id === 'compat-select-box') {
const select = document.getElementById('compat-select');
if (select) {
let newIdx = select.selectedIndex + dir;
if (newIdx < 0) newIdx = select.options.length - 1;
if (newIdx >= select.options.length) newIdx = 0;
select.selectedIndex = newIdx;
updateCompatDisplay();
}
} else if (!this.getActiveModal()) {
// Shortcut for version cycle on main menu
const select = document.getElementById('version-select');
if (select) {
let newIdx = select.selectedIndex + dir;
if (newIdx < 0) newIdx = select.options.length - 1;
if (newIdx >= select.options.length) newIdx = 0;
select.selectedIndex = newIdx;
updateSelectedRelease();
}
}
},
scrollActive(val) {
const serverList = document.getElementById('servers-list-container');
if (this.getActiveModal()?.id === 'servers-modal' && serverList) {
serverList.scrollTop += val;
} else if (!this.getActiveModal()) {
const sidebar = document.getElementById('updates-list')?.parentElement;
if (sidebar) sidebar.scrollTop += val;
}
}
};
window.onload = async () => {
document.getElementById('repo-input').value = await Store.get('legacy_repo', DEFAULT_REPO);
document.getElementById('exec-input').value = await Store.get('legacy_exec_path', DEFAULT_EXEC);
document.getElementById('username-input').value = await Store.get('legacy_username', "");
document.getElementById('ip-input').value = await Store.get('legacy_ip', "");
document.getElementById('port-input').value = await Store.get('legacy_port', "");
document.getElementById('server-checkbox').checked = await Store.get('legacy_is_server', false);
if (process.platform === 'linux' || process.platform === 'darwin') {
document.getElementById('compat-option-container').style.display = 'block';
scanCompatibilityLayers();
} else {
// Force Windows to direct mode if somehow changed
await Store.set('legacy_compat_layer', 'direct');
}
ipcRenderer.on('window-is-maximized', (event, isMaximized) => {
document.getElementById('maximize-btn').textContent = isMaximized ? '❐' : '▢';
});
fetchGitHubData();
checkForLauncherUpdates();
GamepadManager.init();
};
async function checkForLauncherUpdates() {
try {
const currentVersion = require('./package.json').version;
const res = await fetch(`https://api.github.com/repos/${LAUNCHER_REPO}/releases/latest`);
if (!res.ok) return;
const latestRelease = await res.json();
const latestVersion = latestRelease.tag_name.replace('v', '');
if (latestVersion !== currentVersion) {
const updateConfirmed = await promptLauncherUpdate(latestRelease.tag_name);
if (updateConfirmed) {
downloadAndInstallLauncherUpdate(latestRelease);
}
}
} catch (e) {
console.error("Launcher update check failed:", e);
}
}
async function promptLauncherUpdate(version) {
return new Promise((resolve) => {
const modal = document.getElementById('update-modal');
const confirmBtn = document.getElementById('btn-confirm-update');
const skipBtn = document.getElementById('btn-skip-update');
const closeBtn = document.getElementById('btn-close-update');
document.getElementById('update-modal-text').innerHTML =
`A new Launcher version ${version} is available.
` +
`Would you like to download and install it now?`;
modal.style.display = 'flex';
modal.style.opacity = '1';
const cleanup = (result) => {
modal.style.opacity = '0';
setTimeout(() => modal.style.display = 'none', 300);
confirmBtn.onclick = null;
skipBtn.onclick = null;
closeBtn.onclick = null;
resolve(result);
};
confirmBtn.onclick = () => cleanup(true);
skipBtn.onclick = () => cleanup(false);
closeBtn.onclick = () => cleanup(false);
});
}
async function downloadAndInstallLauncherUpdate(release) {
setProcessingState(true);
updateProgress(0, "Preparing Launcher Update...");
let assetPattern = "";
if (process.platform === 'win32') assetPattern = ".exe";
else if (process.platform === 'linux') assetPattern = ".AppImage";
else if (process.platform === 'darwin') assetPattern = ".dmg";
const asset = release.assets.find(a => a.name.toLowerCase().endsWith(assetPattern));
if (!asset) {
showToast("No compatible update found for your OS.");
setProcessingState(false);
return;
}
try {
const homeDir = require('os').homedir();
const downloadPath = path.join(homeDir, 'Downloads', asset.name);
updateProgress(10, `Downloading Launcher Update...`);
await downloadFile(asset.browser_download_url, downloadPath);
updateProgress(100, "Download Complete. Launching Installer...");
// Give time for UI update
await new Promise(r => setTimeout(r, 1000));
if (process.platform === 'win32') {
childProcess.exec(`start "" "${downloadPath}"`);
} else if (process.platform === 'linux') {
fs.chmodSync(downloadPath, 0o755);
childProcess.exec(`"${downloadPath}"`);
} else if (process.platform === 'darwin') {
childProcess.exec(`open "${downloadPath}"`);
}
// Close the app to allow installation
setTimeout(() => ipcRenderer.send('window-close'), 2000);
} catch (e) {
showToast("Launcher Update Error: " + e.message);
setProcessingState(false);
}
}
async function scanCompatibilityLayers() {
const select = document.getElementById('compat-select');
const savedValue = await Store.get('legacy_compat_layer', 'direct');
const layers = [
{ name: 'Default (Direct)', cmd: 'direct' },
{ name: 'Wine64', cmd: 'wine64' },
{ name: 'Wine', cmd: 'wine' }
];
const homeDir = require('os').homedir();
let steamPaths = [];
if (process.platform === 'linux') {
steamPaths = [
path.join(homeDir, '.steam', 'steam', 'steamapps', 'common'),
path.join(homeDir, '.local', 'share', 'Steam', 'steamapps', 'common'),
path.join(homeDir, '.var', 'app', 'com.valvesoftware.Steam', 'data', 'Steam', 'steamapps', 'common')
];
} else if (process.platform === 'darwin') {
steamPaths = [
path.join(homeDir, 'Library', 'Application Support', 'Steam', 'steamapps', 'common')
];
}
for (const steamPath of steamPaths) {
if (fs.existsSync(steamPath)) {
try {
const dirs = fs.readdirSync(steamPath);
dirs.filter(d => d.startsWith('Proton') || d.includes('Wine') || d.includes('CrossOver')).forEach(d => {
// Check for common Proton structure
const protonPath = path.join(steamPath, d, 'proton');
if (fs.existsSync(protonPath)) {
layers.push({ name: d, cmd: protonPath });
}
});
} catch (e) {}
}
}
select.innerHTML = '';
layers.forEach(l => {
const opt = document.createElement('option');
opt.value = l.cmd;
opt.textContent = l.name;
select.appendChild(opt);
if (l.cmd === savedValue) opt.selected = true;
});
updateCompatDisplay();
}
function updateCompatDisplay() {
const select = document.getElementById('compat-select');
const display = document.getElementById('current-compat-display');
if (select && display && select.selectedIndex !== -1) {
display.textContent = select.options[select.selectedIndex].text;
}
}
async function getInstalledPath() {
const homeDir = require('os').homedir();
const execPath = await Store.get('legacy_exec_path', DEFAULT_EXEC);
return path.join(homeDir, 'Documents', 'LegacyClient', execPath);
}
async function checkIsInstalled(tag) {
const fullPath = await getInstalledPath();
const installedTag = await Store.get('installed_version_tag');
return fs.existsSync(fullPath) && installedTag === tag;
}
async function updatePlayButtonText() {
const btn = document.getElementById('btn-play-main');
if (isProcessing) return;
if (isGameRunning) {
btn.textContent = "GAME RUNNING";
btn.classList.add('running');
return;
} else {
btn.classList.remove('running');
}
const release = releasesData[currentReleaseIndex];
if (!release) {
btn.textContent = "PLAY";
return;
}
if (await checkIsInstalled(release.tag_name)) {
btn.textContent = "PLAY";
} else {
const fullPath = await getInstalledPath();
if (fs.existsSync(fullPath)) {
btn.textContent = "UPDATE";
} else {
btn.textContent = "INSTALL";
}
}
}
function setGameRunning(running) {
isGameRunning = running;
updatePlayButtonText();
}
async function monitorProcess(proc) {
if (!proc) return;
const sessionStart = Date.now();
setGameRunning(true);
proc.on('exit', async () => {
const sessionDuration = Math.floor((Date.now() - sessionStart) / 1000);
const playtime = await Store.get('legacy_playtime', 0);
await Store.set('legacy_playtime', playtime + sessionDuration);
setGameRunning(false);
});
proc.on('error', (err) => {
console.error("Process error:", err);
setGameRunning(false);
});
}
function minimizeWindow() {
ipcRenderer.send('window-minimize');
}
function toggleMaximize() {
ipcRenderer.send('window-maximize');
}
function closeWindow() {
ipcRenderer.send('window-close');
}
async function fetchGitHubData() {
const repo = await Store.get('legacy_repo', DEFAULT_REPO);
const loader = document.getElementById('loader');
const loaderText = document.getElementById('loader-text');
loader.style.display = 'flex';
loaderText.textContent = "SYNCING: " + repo;
try {
const [relRes, commRes] = await Promise.all([
fetch(`https://api.github.com/repos/${repo}/releases`),
fetch(`https://api.github.com/repos/${repo}/commits`)
]);
if (!relRes.ok || !commRes.ok) throw new Error("Rate Limited or API Error");
releasesData = await relRes.json();
commitsData = await commRes.json();
populateVersions();
populateUpdatesSidebar();
setTimeout(() => {
loader.style.opacity = '0';
setTimeout(() => loader.style.display = 'none', 300);
}, 500);
} catch (err) {
loaderText.textContent = "REPO NOT FOUND OR API ERROR";
showToast("Check repository name in Options.");
setTimeout(() => {
loader.style.opacity = '0';
setTimeout(() => loader.style.display = 'none', 300);
}, 2500);
}
}
function populateVersions() {
const select = document.getElementById('version-select');
const display = document.getElementById('current-version-display');
select.innerHTML = '';
if(releasesData.length === 0) {
display.textContent = "No Releases Found";
return;
}
releasesData.forEach((rel, index) => {
const opt = document.createElement('option');
opt.value = index;
opt.textContent = `Legacy (${rel.tag_name})`;
select.appendChild(opt);
if(index === 0) display.textContent = opt.textContent;
});
currentReleaseIndex = 0;
updatePlayButtonText();
}
function populateUpdatesSidebar() {
const list = document.getElementById('updates-list');
list.innerHTML = '';
if (commitsData.length === 0) {
list.innerHTML = '