From 21f8527ed424570b15c08dca281b993c3aac93d7 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Mon, 19 Jan 2026 23:15:29 +0100 Subject: [PATCH] update 2.0.2 --- GUI/index.html | 190 +++++- GUI/js/chat.js | 154 ++++- GUI/js/install.js | 18 +- GUI/js/launcher.js | 194 ++++-- GUI/js/mods.js | 1 - GUI/js/players.js | 2 +- GUI/js/script.js | 6 +- GUI/js/settings.js | 595 ++++++++++++++++++- GUI/style.css | 783 +++++++++++++++++++++++++ README.md | 36 +- SERVER.md | 473 +++++++++++++-- backend/core/config.js | 120 +++- backend/launcher.js | 30 +- backend/managers/gameLauncher.js | 148 ++++- backend/managers/gameManager.js | 10 +- backend/managers/multiClientManager.js | 86 --- backend/managers/uiFileManager.js | 4 +- backend/services/versionManager.js | 32 +- backend/updateManager.js | 4 +- backend/utils/clientPatcher.js | 468 +++++++++++++++ backend/utils/fileManager.js | 178 ++++-- main.js | 174 +++++- package.json | 2 +- preload.js | 14 +- 24 files changed, 3376 insertions(+), 346 deletions(-) create mode 100644 backend/utils/clientPatcher.js diff --git a/GUI/index.html b/GUI/index.html index 8d981e6..10b33d3 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -205,9 +205,15 @@ PLAYERS CHAT -
- - 0 online +
+ +
+ + 0 online +
@@ -291,6 +297,24 @@ +
+

+ + Discord Integration +

+ +
+ +
+
+

@@ -326,6 +350,50 @@

+ +
+

+ + Player UUID Management +

+ +
+
+ +
+ + + +
+

+ + Your unique player identifier for this username +

+
+
+ +
+
+ +
+
+
@@ -415,16 +483,84 @@ + + + @@ -442,6 +578,50 @@ + + + + diff --git a/GUI/js/chat.js b/GUI/js/chat.js index 90bd1d8..0679379 100644 --- a/GUI/js/chat.js +++ b/GUI/js/chat.js @@ -3,13 +3,26 @@ let socket = null; let isAuthenticated = false; let messageQueue = []; let chatUsername = ''; -const SOCKET_URL = 'http://3.10.208.30:3001'; +let userColor = '#3498db'; +let userBadge = null; +const SOCKET_URL = 'https://chat.hytalef2p.com'; const MAX_MESSAGE_LENGTH = 500; +async function getOrCreatePlayerId() { + return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +} + export async function initChat() { if (window.electronAPI?.loadChatUsername) { chatUsername = await window.electronAPI.loadChatUsername(); } + + if (window.electronAPI?.loadChatColor) { + const savedColor = await window.electronAPI.loadChatColor(); + if (savedColor) { + userColor = savedColor; + } + } if (!chatUsername || chatUsername.trim() === '') { showUsernameModal(); @@ -17,6 +30,7 @@ export async function initChat() { } setupChatUI(); + setupColorSelector(); await connectToChat(); } @@ -136,13 +150,22 @@ async function connectToChat() { reconnectionDelay: 1000 }); - socket.on('connect', () => { + socket.on('connect', async () => { console.log('Connected to chat server'); - socket.emit('authenticate', { username: chatUsername, userId }); + + const uuid = await window.electronAPI?.getCurrentUuid(); + + socket.emit('authenticate', { + username: chatUsername, + userId, + uuid: uuid, + userColor: userColor + }); }); socket.on('authenticated', (data) => { isAuthenticated = true; + userBadge = data.badge; addSystemMessage(`Connected as ${data.username}`); while (messageQueue.length > 0) { @@ -155,7 +178,7 @@ async function connectToChat() { if (data.type === 'system') { addSystemMessage(data.message); } else if (data.type === 'user') { - addUserMessage(data.username, data.message, data.timestamp); + addUserMessage(data.username, data.message, data.timestamp, data.userColor, data.badge); } }); @@ -226,7 +249,7 @@ function sendMessage() { updateCharCounter(); } -function addUserMessage(username, message, timestamp) { +function addUserMessage(username, message, timestamp, userColor = '#3498db', badge = null) { const chatMessages = document.getElementById('chatMessages'); if (!chatMessages) return; @@ -238,14 +261,35 @@ function addUserMessage(username, message, timestamp) { minute: '2-digit' }); + let badgeHTML = ''; + if (badge) { + let badgeStyle = ''; + if (badge.style === 'rainbow') { + badgeStyle = `background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4, #ffeaa7, #fab1a0, #fd79a8); background-size: 400% 400%; animation: rainbow 3s ease infinite; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: bold; display: inline;`; + } else if (badge.style === 'gradient') { + if (badge.badge === 'CONTRIBUTOR') { + badgeStyle = `background: linear-gradient(45deg, #22c55e, #16a34a); background-size: 200% 200%; animation: contributorGlow 2s ease infinite; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: bold; display: inline;`; + } else { + badgeStyle = `color: ${badge.color}; font-weight: bold; display: inline;`; + } + } + + badgeHTML = `[${badge.badge}] `; + } + messageDiv.innerHTML = `
- ${escapeHtml(username)} + ${time}
${message}
`; + const usernameElement = messageDiv.querySelector('.message-username'); + if (usernameElement) { + applyUserColorStyle(usernameElement, userColor); + } + chatMessages.appendChild(messageDiv); scrollToBottom(); } @@ -352,6 +396,104 @@ document.addEventListener('DOMContentLoaded', () => { } }); +function setupColorSelector() { + const colorBtn = document.getElementById('chatColorBtn'); + if (colorBtn) { + colorBtn.addEventListener('click', showChatColorModal); + } + + const colorOptions = document.querySelectorAll('.color-option'); + colorOptions.forEach(option => { + option.addEventListener('click', () => { + document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected')); + option.classList.add('selected'); + updateColorPreview(); + }); + }); + + const customColor = document.getElementById('customColor'); + if (customColor) { + customColor.addEventListener('input', () => { + document.querySelectorAll('.color-option').forEach(o => o.classList.remove('selected')); + updateColorPreview(); + }); + } +} + +function showChatColorModal() { + const modal = document.getElementById('chatColorModal'); + if (modal) { + modal.style.display = 'flex'; + updateColorPreview(); + } +} + +window.closeChatColorModal = function() { + const modal = document.getElementById('chatColorModal'); + if (modal) { + modal.style.display = 'none'; + } +} + +function updateColorPreview() { + const preview = document.getElementById('colorPreview'); + if (!preview) return; + + const selectedOption = document.querySelector('.color-option.selected'); + let color = '#3498db'; + + if (selectedOption) { + color = selectedOption.dataset.color; + } else { + const customColor = document.getElementById('customColor'); + if (customColor) color = customColor.value; + } + + preview.style.color = color; + preview.style.background = 'transparent'; + preview.style.webkitBackgroundClip = 'initial'; + preview.style.webkitTextFillColor = 'initial'; +} + +window.applyChatColor = async function() { + let newColor; + + const selectedOption = document.querySelector('.color-option.selected'); + if (selectedOption) { + newColor = selectedOption.dataset.color; + } else { + const customColor = document.getElementById('customColor'); + newColor = customColor ? customColor.value : '#3498db'; + } + + userColor = newColor; + + if (window.electronAPI?.saveChatColor) { + await window.electronAPI.saveChatColor(newColor); + } + + if (socket && isAuthenticated) { + const uuid = await window.electronAPI?.getCurrentUuid(); + socket.emit('authenticate', { + username: chatUsername, + userId: await getOrCreatePlayerId(), + uuid: uuid, + userColor: userColor + }); + + addSystemMessage('Username color updated successfully', 'success'); + } + + closeChatColorModal(); +} + +function applyUserColorStyle(element, color) { + element.style.color = color; + element.style.background = 'transparent'; + element.style.webkitBackgroundClip = 'initial'; + element.style.webkitTextFillColor = 'initial'; +} + window.ChatAPI = { send: sendMessage, disconnect: () => socket?.disconnect() diff --git a/GUI/js/install.js b/GUI/js/install.js index c6f728e..79a51ef 100644 --- a/GUI/js/install.js +++ b/GUI/js/install.js @@ -33,21 +33,12 @@ export function setupInstallation() { if (window.electronAPI && window.electronAPI.onProgressUpdate) { window.electronAPI.onProgressUpdate((data) => { + if (!isDownloading) return; if (window.LauncherUI) { - window.LauncherUI.showProgress(); window.LauncherUI.updateProgress(data); } }); } - - if (window.electronAPI && window.electronAPI.onProgressComplete) { - window.electronAPI.onProgressComplete(() => { - if (window.LauncherUI) { - window.LauncherUI.hideProgress(); - } - resetInstallButton(); - }); - } } export async function installGame() { @@ -75,6 +66,7 @@ export async function installGame() { window.LauncherUI.showLauncherOrInstall(true); const playerNameInput = document.getElementById('playerName'); if (playerNameInput) playerNameInput.value = playerName; + resetInstallButton(); }, 2000); } } else { @@ -210,3 +202,9 @@ document.addEventListener('DOMContentLoaded', async () => { setupInstallation(); await checkGameStatusAndShowInterface(); }); +window.browseInstallPath = browseInstallPath; + +document.addEventListener('DOMContentLoaded', async () => { + setupInstallation(); + await checkGameStatusAndShowInterface(); +}); diff --git a/GUI/js/launcher.js b/GUI/js/launcher.js index 5795770..ffd5f18 100644 --- a/GUI/js/launcher.js +++ b/GUI/js/launcher.js @@ -25,21 +25,12 @@ export function setupLauncher() { if (window.electronAPI && window.electronAPI.onProgressUpdate) { window.electronAPI.onProgressUpdate((data) => { + if (!isDownloading) return; if (window.LauncherUI) { - window.LauncherUI.showProgress(); window.LauncherUI.updateProgress(data); } }); } - - if (window.electronAPI && window.electronAPI.onProgressComplete) { - window.electronAPI.onProgressComplete(() => { - if (window.LauncherUI) { - window.LauncherUI.hideProgress(); - } - resetPlayButton(); - }); - } } export async function launch() { @@ -65,48 +56,181 @@ export async function launch() { } try { + if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Starting game...' }); + if (window.electronAPI && window.electronAPI.launchGame) { const result = await window.electronAPI.launchGame(playerName, javaPath, ''); + isDownloading = false; + + if (window.LauncherUI) { + window.LauncherUI.hideProgress(); + } + resetPlayButton(); + if (result.success) { - if (window.LauncherUI) { - window.LauncherUI.updateProgress({ message: 'Game started successfully!' }); + if (window.electronAPI.minimizeWindow) { setTimeout(() => { - window.LauncherUI.hideProgress(); - if (window.electronAPI.minimizeWindow) { - window.electronAPI.minimizeWindow(); - } - }, 2000); + window.electronAPI.minimizeWindow(); + }, 500); } } else { - throw new Error(result.error || 'Launch failed'); + console.error('Launch failed:', result.error); } } else { - setTimeout(() => { - if (window.LauncherUI) { - window.LauncherUI.updateProgress({ message: 'Game started successfully!' }); - setTimeout(() => { - window.LauncherUI.hideProgress(); - resetPlayButton(); - }, 2000); - } - }, 2000); + isDownloading = false; + + if (window.LauncherUI) { + window.LauncherUI.hideProgress(); + } + resetPlayButton(); } } catch (error) { + isDownloading = false; + if (window.LauncherUI) { - window.LauncherUI.updateProgress({ message: `Failed: ${error.message}` }); - setTimeout(() => { - window.LauncherUI.hideProgress(); - resetPlayButton(); - }, 3000); + window.LauncherUI.hideProgress(); } + resetPlayButton(); + console.error('Launch error:', error); } } -export async function uninstallGame() { - if (!confirm('Are you sure you want to uninstall Hytale? All game files will be deleted.')) { - return; +function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCancel = null, confirmText = 'Confirm', cancelText = 'Cancel') { + const existingModal = document.querySelector('.custom-confirm-modal'); + if (existingModal) { + existingModal.remove(); } + + const modal = document.createElement('div'); + modal.className = 'custom-confirm-modal'; + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(4px); + z-index: 20000; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s ease; + `; + + const dialog = document.createElement('div'); + dialog.className = 'custom-confirm-dialog'; + dialog.style.cssText = ` + background: #1f2937; + border-radius: 12px; + padding: 0; + min-width: 400px; + max-width: 500px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6); + border: 1px solid rgba(239, 68, 68, 0.3); + transform: scale(0.9); + transition: transform 0.3s ease; + `; + + dialog.innerHTML = ` +
+
+ +

${title}

+
+
+
+

${message}

+
+
+ + +
+ `; + + modal.appendChild(dialog); + document.body.appendChild(modal); + + // Animate in + setTimeout(() => { + modal.style.opacity = '1'; + dialog.style.transform = 'scale(1)'; + }, 10); + + // Event handlers + const cancelBtn = dialog.querySelector('.custom-confirm-cancel'); + const actionBtn = dialog.querySelector('.custom-confirm-action'); + + const closeModal = () => { + modal.style.opacity = '0'; + dialog.style.transform = 'scale(0.9)'; + setTimeout(() => { + modal.remove(); + }, 300); + }; + + cancelBtn.onclick = () => { + closeModal(); + if (onCancel) onCancel(); + }; + + actionBtn.onclick = () => { + closeModal(); + onConfirm(); + }; + + modal.onclick = (e) => { + if (e.target === modal) { + closeModal(); + if (onCancel) onCancel(); + } + }; + + // Escape key + const handleEscape = (e) => { + if (e.key === 'Escape') { + closeModal(); + if (onCancel) onCancel(); + document.removeEventListener('keydown', handleEscape); + } + }; + document.addEventListener('keydown', handleEscape); +} + +export async function uninstallGame() { + showCustomConfirm( + 'Are you sure you want to uninstall Hytale? All game files will be deleted.', + 'Uninstall Game', + async () => { + await performUninstall(); + }, + null, + 'Uninstall', + 'Cancel' + ); +} + +async function performUninstall() { if (window.LauncherUI) window.LauncherUI.showProgress(); if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Uninstalling game...' }); diff --git a/GUI/js/mods.js b/GUI/js/mods.js index 9f5d540..8c0a895 100644 --- a/GUI/js/mods.js +++ b/GUI/js/mods.js @@ -522,7 +522,6 @@ function showNotification(message, type = 'info', duration = 4000) { }, duration); } -// Custom confirmation modal function showConfirmModal(message, onConfirm, onCancel = null) { const existingModal = document.querySelector('.mod-confirm-modal'); if (existingModal) { diff --git a/GUI/js/players.js b/GUI/js/players.js index 84efc0a..4195176 100644 --- a/GUI/js/players.js +++ b/GUI/js/players.js @@ -1,5 +1,5 @@ -const API_URL = 'http://3.10.208.30/api'; +const API_URL = 'https://api.hytalef2p.com/api'; let updateInterval = null; let currentUserId = null; diff --git a/GUI/js/script.js b/GUI/js/script.js index b7d485d..0d90bef 100644 --- a/GUI/js/script.js +++ b/GUI/js/script.js @@ -7,7 +7,6 @@ import './players.js'; import './chat.js'; import './settings.js'; -// Discord notification functions window.closeDiscordNotification = function() { const notification = document.getElementById('discordNotification'); if (notification) { @@ -18,23 +17,20 @@ window.closeDiscordNotification = function() { } }; -// Show notification after a delay document.addEventListener('DOMContentLoaded', () => { const notification = document.getElementById('discordNotification'); if (notification) { - // Check if user has previously dismissed the notification const dismissed = localStorage.getItem('discordNotificationDismissed'); if (!dismissed) { setTimeout(() => { notification.style.display = 'flex'; - }, 3000); // Show after 3 seconds + }, 3000); } else { notification.style.display = 'none'; } } }); -// Remember when user closes notification const originalClose = window.closeDiscordNotification; window.closeDiscordNotification = function() { localStorage.setItem('discordNotificationDismissed', 'true'); diff --git a/GUI/js/settings.js b/GUI/js/settings.js index 701b252..0910734 100644 --- a/GUI/js/settings.js +++ b/GUI/js/settings.js @@ -4,6 +4,143 @@ let customJavaOptions; let customJavaPath; let browseJavaBtn; let settingsPlayerName; +let discordRPCCheck; + +// UUID Management elements +let currentUuidDisplay; +let copyUuidBtn; +let regenerateUuidBtn; +let manageUuidsBtn; +let uuidModal; +let uuidModalClose; +let modalCurrentUuid; +let modalCopyUuidBtn; +let modalRegenerateUuidBtn; +let generateNewUuidBtn; +let uuidList; +let customUuidInput; +let setCustomUuidBtn; + +function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCancel = null, confirmText = 'Confirm', cancelText = 'Cancel') { + const existingModal = document.querySelector('.custom-confirm-modal'); + if (existingModal) { + existingModal.remove(); + } + + const modal = document.createElement('div'); + modal.className = 'custom-confirm-modal'; + modal.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(4px); + z-index: 20000; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s ease; + `; + + const dialog = document.createElement('div'); + dialog.className = 'custom-confirm-dialog'; + dialog.style.cssText = ` + background: #1f2937; + border-radius: 12px; + padding: 0; + min-width: 400px; + max-width: 500px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6); + border: 1px solid rgba(147, 51, 234, 0.3); + transform: scale(0.9); + transition: transform 0.3s ease; + `; + + dialog.innerHTML = ` +
+
+ +

${title}

+
+
+
+

${message}

+
+
+ + +
+ `; + + modal.appendChild(dialog); + document.body.appendChild(modal); + + // Animate in + setTimeout(() => { + modal.style.opacity = '1'; + dialog.style.transform = 'scale(1)'; + }, 10); + + // Event handlers + const cancelBtn = dialog.querySelector('.custom-confirm-cancel'); + const actionBtn = dialog.querySelector('.custom-confirm-action'); + + const closeModal = () => { + modal.style.opacity = '0'; + dialog.style.transform = 'scale(0.9)'; + setTimeout(() => { + modal.remove(); + }, 300); + }; + + cancelBtn.onclick = () => { + closeModal(); + if (onCancel) onCancel(); + }; + + actionBtn.onclick = () => { + closeModal(); + onConfirm(); + }; + + modal.onclick = (e) => { + if (e.target === modal) { + closeModal(); + if (onCancel) onCancel(); + } + }; + + // Escape key + const handleEscape = (e) => { + if (e.key === 'Escape') { + closeModal(); + if (onCancel) onCancel(); + document.removeEventListener('keydown', handleEscape); + } + }; + document.addEventListener('keydown', handleEscape); +} export function initSettings() { setupSettingsElements(); @@ -16,6 +153,22 @@ function setupSettingsElements() { customJavaPath = document.getElementById('customJavaPath'); browseJavaBtn = document.getElementById('browseJavaBtn'); settingsPlayerName = document.getElementById('settingsPlayerName'); + discordRPCCheck = document.getElementById('discordRPCCheck'); + + // UUID Management elements + currentUuidDisplay = document.getElementById('currentUuid'); + copyUuidBtn = document.getElementById('copyUuidBtn'); + regenerateUuidBtn = document.getElementById('regenerateUuidBtn'); + manageUuidsBtn = document.getElementById('manageUuidsBtn'); + uuidModal = document.getElementById('uuidModal'); + uuidModalClose = document.getElementById('uuidModalClose'); + modalCurrentUuid = document.getElementById('modalCurrentUuid'); + modalCopyUuidBtn = document.getElementById('modalCopyUuidBtn'); + modalRegenerateUuidBtn = document.getElementById('modalRegenerateUuidBtn'); + generateNewUuidBtn = document.getElementById('generateNewUuidBtn'); + uuidList = document.getElementById('uuidList'); + customUuidInput = document.getElementById('customUuidInput'); + setCustomUuidBtn = document.getElementById('setCustomUuidBtn'); if (customJavaCheck) { customJavaCheck.addEventListener('change', toggleCustomJava); @@ -28,6 +181,51 @@ function setupSettingsElements() { if (settingsPlayerName) { settingsPlayerName.addEventListener('change', savePlayerName); } + + if (discordRPCCheck) { + discordRPCCheck.addEventListener('change', saveDiscordRPC); + } + + // UUID event listeners + if (copyUuidBtn) { + copyUuidBtn.addEventListener('click', copyCurrentUuid); + } + + if (regenerateUuidBtn) { + regenerateUuidBtn.addEventListener('click', regenerateCurrentUuid); + } + + if (manageUuidsBtn) { + manageUuidsBtn.addEventListener('click', openUuidModal); + } + + if (uuidModalClose) { + uuidModalClose.addEventListener('click', closeUuidModal); + } + + if (modalCopyUuidBtn) { + modalCopyUuidBtn.addEventListener('click', copyCurrentUuid); + } + + if (modalRegenerateUuidBtn) { + modalRegenerateUuidBtn.addEventListener('click', regenerateCurrentUuid); + } + + if (generateNewUuidBtn) { + generateNewUuidBtn.addEventListener('click', generateNewUuid); + } + + if (setCustomUuidBtn) { + setCustomUuidBtn.addEventListener('click', setCustomUuid); + } + + if (uuidModal) { + uuidModal.addEventListener('click', (e) => { + if (e.target === uuidModal) { + closeUuidModal(); + } + }); + } } function toggleCustomJava() { @@ -90,24 +288,73 @@ async function loadCustomJavaPath() { } } -async function savePlayerName() { +async function saveDiscordRPC() { try { - if (window.electronAPI && window.electronAPI.saveUsername && settingsPlayerName) { - const playerName = settingsPlayerName.value.trim() || 'Player'; - await window.electronAPI.saveUsername(playerName); + if (window.electronAPI && window.electronAPI.saveDiscordRPC && discordRPCCheck) { + const enabled = discordRPCCheck.checked; + console.log('Saving Discord RPC setting:', enabled); + + const result = await window.electronAPI.saveDiscordRPC(enabled); + + if (result && result.success) { + console.log('Discord RPC setting saved successfully:', enabled); + + // Feedback visuel pour l'utilisateur + if (enabled) { + showNotification('Discord Rich Presence enabled', 'success'); + } else { + showNotification('Discord Rich Presence disabled', 'success'); + } + } else { + throw new Error('Failed to save Discord RPC setting'); + } } + } catch (error) { + console.error('Error saving Discord RPC setting:', error); + showNotification('Failed to save Discord setting', 'error'); + } +} + +async function loadDiscordRPC() { + try { + if (window.electronAPI && window.electronAPI.loadDiscordRPC) { + const enabled = await window.electronAPI.loadDiscordRPC(); + if (discordRPCCheck) { + discordRPCCheck.checked = enabled; + } + } + } catch (error) { + console.error('Error loading Discord RPC setting:', error); + } +} + +async function savePlayerName() { + try { + if (!window.electronAPI || !settingsPlayerName) return; + + const playerName = settingsPlayerName.value.trim(); + + if (!playerName) { + showNotification('Please enter a valid player name', 'error'); + return; + } + + await window.electronAPI.saveUsername(playerName); + showNotification('Player name saved successfully', 'success'); + } catch (error) { console.error('Error saving player name:', error); + showNotification('Failed to save player name', 'error'); } } async function loadPlayerName() { try { - if (window.electronAPI && window.electronAPI.loadUsername && settingsPlayerName) { - const savedName = await window.electronAPI.loadUsername(); - if (savedName) { - settingsPlayerName.value = savedName; - } + if (!window.electronAPI || !settingsPlayerName) return; + + const savedName = await window.electronAPI.loadUsername(); + if (savedName) { + settingsPlayerName.value = savedName; } } catch (error) { console.error('Error loading player name:', error); @@ -117,6 +364,8 @@ async function loadPlayerName() { async function loadAllSettings() { await loadCustomJavaPath(); await loadPlayerName(); + await loadCurrentUuid(); + await loadDiscordRPC(); } async function openGameLocation() { @@ -144,7 +393,6 @@ export function getCurrentPlayerName() { return 'Player'; } -// Make openGameLocation globally available window.openGameLocation = openGameLocation; document.addEventListener('DOMContentLoaded', initSettings); @@ -152,4 +400,329 @@ document.addEventListener('DOMContentLoaded', initSettings); window.SettingsAPI = { getCurrentJavaPath, getCurrentPlayerName -}; \ No newline at end of file +}; + +async function loadCurrentUuid() { + try { + if (window.electronAPI && window.electronAPI.getCurrentUuid) { + const uuid = await window.electronAPI.getCurrentUuid(); + if (uuid) { + if (currentUuidDisplay) currentUuidDisplay.value = uuid; + if (modalCurrentUuid) modalCurrentUuid.value = uuid; + } + } + } catch (error) { + console.error('Error loading current UUID:', error); + } +} + +async function copyCurrentUuid() { + try { + const uuid = currentUuidDisplay ? currentUuidDisplay.value : modalCurrentUuid?.value; + if (uuid && navigator.clipboard) { + await navigator.clipboard.writeText(uuid); + showNotification('UUID copied to clipboard!', 'success'); + } + } catch (error) { + console.error('Error copying UUID:', error); + showNotification('Failed to copy UUID', 'error'); + } +} + +async function regenerateCurrentUuid() { + try { + if (window.electronAPI && window.electronAPI.resetCurrentUserUuid) { + showCustomConfirm( + 'Are you sure you want to generate a new UUID? This will change your player identity.', + 'Generate New UUID', + async () => { + await performRegenerateUuid(); + }, + null, + 'Generate', + 'Cancel' + ); + } else { + console.error('electronAPI.resetCurrentUserUuid not available'); + showNotification('UUID regeneration not available', 'error'); + } + } catch (error) { + console.error('Error in regenerateCurrentUuid:', error); + showNotification('Failed to regenerate UUID', 'error'); + } +} + +async function performRegenerateUuid() { + try { + const result = await window.electronAPI.resetCurrentUserUuid(); + if (result.success && result.uuid) { + if (currentUuidDisplay) currentUuidDisplay.value = result.uuid; + if (modalCurrentUuid) modalCurrentUuid.value = result.uuid; + showNotification('New UUID generated successfully!', 'success'); + + if (uuidModal && uuidModal.style.display !== 'none') { + await loadAllUuids(); + } + } else { + throw new Error(result.error || 'Failed to generate new UUID'); + } + } catch (error) { + console.error('Error regenerating UUID:', error); + showNotification(`Failed to regenerate UUID: ${error.message}`, 'error'); + } +} + +async function openUuidModal() { + try { + if (uuidModal) { + uuidModal.style.display = 'flex'; + uuidModal.classList.add('active'); + await loadAllUuids(); + } + } catch (error) { + console.error('Error opening UUID modal:', error); + } +} + +function closeUuidModal() { + if (uuidModal) { + uuidModal.classList.remove('active'); + setTimeout(() => { + uuidModal.style.display = 'none'; + }, 300); + } +} + +async function loadAllUuids() { + try { + if (!uuidList) return; + + uuidList.innerHTML = ` +
+ + Loading UUIDs... +
+ `; + + if (window.electronAPI && window.electronAPI.getAllUuidMappings) { + const mappings = await window.electronAPI.getAllUuidMappings(); + + if (mappings.length === 0) { + uuidList.innerHTML = ` +
+ + No UUIDs found +
+ `; + return; + } + + uuidList.innerHTML = ''; + + for (const mapping of mappings) { + const item = document.createElement('div'); + item.className = `uuid-list-item${mapping.isCurrent ? ' current' : ''}`; + + item.innerHTML = ` +
+
${escapeHtml(mapping.username)}
+
${mapping.uuid}
+
+
+ ${mapping.isCurrent ? '
Current
' : ''} + + ${!mapping.isCurrent ? `` : ''} +
+ `; + + uuidList.appendChild(item); + } + } + } catch (error) { + console.error('Error loading UUIDs:', error); + if (uuidList) { + uuidList.innerHTML = ` +
+ + Error loading UUIDs +
+ `; + } + } +} + +async function generateNewUuid() { + try { + if (window.electronAPI && window.electronAPI.generateNewUuid) { + const newUuid = await window.electronAPI.generateNewUuid(); + if (newUuid) { + if (customUuidInput) customUuidInput.value = newUuid; + showNotification('New UUID generated!', 'success'); + } + } + } catch (error) { + console.error('Error generating new UUID:', error); + showNotification('Failed to generate new UUID', 'error'); + } +} + +async function setCustomUuid() { + try { + if (!customUuidInput || !customUuidInput.value.trim()) { + showNotification('Please enter a UUID', 'error'); + return; + } + + const uuid = customUuidInput.value.trim(); + + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!uuidRegex.test(uuid)) { + showNotification('Invalid UUID format', 'error'); + return; + } + + showCustomConfirm( + 'Are you sure you want to set this custom UUID? This will change your player identity.', + 'Set Custom UUID', + async () => { + await performSetCustomUuid(uuid); + }, + null, + 'Set UUID', + 'Cancel' + ); + } catch (error) { + console.error('Error in setCustomUuid:', error); + showNotification('Failed to set custom UUID', 'error'); + } +} + + async function performSetCustomUuid(uuid) { + try { + if (window.electronAPI && window.electronAPI.setUuidForUser) { + const username = getCurrentPlayerName(); + const result = await window.electronAPI.setUuidForUser(username, uuid); + + if (result.success) { + if (currentUuidDisplay) currentUuidDisplay.value = uuid; + if (modalCurrentUuid) modalCurrentUuid.value = uuid; + if (customUuidInput) customUuidInput.value = ''; + + showNotification('Custom UUID set successfully!', 'success'); + + await loadAllUuids(); + } else { + throw new Error(result.error || 'Failed to set custom UUID'); + } + } + } catch (error) { + console.error('Error setting custom UUID:', error); + showNotification(`Failed to set custom UUID: ${error.message}`, 'error'); + } + } + +window.copyUuid = async function(uuid) { + try { + if (navigator.clipboard) { + await navigator.clipboard.writeText(uuid); + showNotification('UUID copied to clipboard!', 'success'); + } + } catch (error) { + console.error('Error copying UUID:', error); + showNotification('Failed to copy UUID', 'error'); + } +}; + +window.deleteUuid = async function(username) { + try { + showCustomConfirm( + `Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`, + 'Delete UUID', + async () => { + await performDeleteUuid(username); + }, + null, + 'Delete', + 'Cancel' + ); + } catch (error) { + console.error('Error in deleteUuid:', error); + showNotification('Failed to delete UUID', 'error'); + } +}; + +async function performDeleteUuid(username) { + try { + if (window.electronAPI && window.electronAPI.deleteUuidForUser) { + const result = await window.electronAPI.deleteUuidForUser(username); + + if (result.success) { + showNotification('UUID deleted successfully!', 'success'); + await loadAllUuids(); + } else { + throw new Error(result.error || 'Failed to delete UUID'); + } + } + } catch (error) { + console.error('Error deleting UUID:', error); + showNotification(`Failed to delete UUID: ${error.message}`, 'error'); + } +} + +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +function showNotification(message, type = 'info') { + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + padding: 1rem 1.5rem; + border-radius: 8px; + color: white; + font-weight: 600; + z-index: 10000; + opacity: 0; + transform: translateX(100%); + transition: all 0.3s ease; + `; + + if (type === 'success') { + notification.style.background = 'linear-gradient(135deg, #22c55e, #16a34a)'; + } else if (type === 'error') { + notification.style.background = 'linear-gradient(135deg, #ef4444, #dc2626)'; + } else { + notification.style.background = 'linear-gradient(135deg, #3b82f6, #2563eb)'; + } + + notification.innerHTML = ` + + ${message} + `; + + document.body.appendChild(notification); + + setTimeout(() => { + notification.style.opacity = '1'; + notification.style.transform = 'translateX(0)'; + }, 100); + + setTimeout(() => { + notification.style.opacity = '0'; + notification.style.transform = 'translateX(100%)'; + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 300); + }, 3000); +} \ No newline at end of file diff --git a/GUI/style.css b/GUI/style.css index 444b871..77d1483 100644 --- a/GUI/style.css +++ b/GUI/style.css @@ -3407,6 +3407,12 @@ body { margin-bottom: 0.5rem; } +.message-user-info { + display: flex; + align-items: center; + flex-wrap: nowrap; +} + .message-username { font-weight: 600; color: #9333ea; @@ -4330,4 +4336,781 @@ body { } } +/* UUID Management Styles */ +.uuid-display-container { + display: flex; + align-items: stretch; + gap: 0.5rem; +} +.uuid-input { + flex: 1; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.2); + color: rgba(255, 255, 255, 0.9); + font-family: 'JetBrains Mono', monospace; + font-size: 0.875rem; + letter-spacing: 0.5px; +} + +.uuid-btn { + padding: 0.875rem; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + min-width: 44px; +} + +.uuid-btn:hover { + background: rgba(147, 51, 234, 0.2); + border-color: rgba(147, 51, 234, 0.4); + color: #9333ea; + transform: translateY(-2px); +} + +.copy-btn:hover { + background: rgba(34, 197, 94, 0.2); + border-color: rgba(34, 197, 94, 0.4); + color: #22c55e; +} + +.regenerate-btn:hover { + background: rgba(249, 115, 22, 0.2); + border-color: rgba(249, 115, 22, 0.4); + color: #f97316; +} + +/* UUID Modal Styles */ +.uuid-modal { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); + z-index: 9999; + align-items: center; + justify-content: center; + animation: fadeIn 0.3s ease; +} + +.uuid-modal.active { + display: flex; +} + +.uuid-modal-content { + background: rgba(10, 10, 10, 0.95); + border: 1px solid rgba(147, 51, 234, 0.3); + border-radius: 16px; + width: 90%; + max-width: 800px; + max-height: 80vh; + display: flex; + flex-direction: column; + animation: slideUp 0.3s ease; + box-shadow: 0 20px 60px rgba(147, 51, 234, 0.3); +} + +.uuid-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.5rem 2rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.uuid-modal-title { + font-size: 1.5rem; + font-weight: 700; + font-family: 'Space Grotesk', sans-serif; + color: white; + display: flex; + align-items: center; + margin: 0; +} + +.modal-close-btn { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: rgba(255, 255, 255, 0.6); + cursor: pointer; + transition: all 0.3s ease; +} + +.modal-close-btn:hover { + background: rgba(255, 0, 0, 0.2); + border-color: rgba(255, 0, 0, 0.3); + color: rgb(239, 68, 68); +} + +.uuid-modal-body { + padding: 2rem; + overflow-y: auto; + flex: 1; + display: flex; + flex-direction: column; + gap: 2rem; +} + +.uuid-modal-body::-webkit-scrollbar { + width: 8px; +} + +.uuid-modal-body::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; +} + +.uuid-modal-body::-webkit-scrollbar-thumb { + background: rgba(147, 51, 234, 0.3); + border-radius: 4px; +} + +.uuid-modal-body::-webkit-scrollbar-thumb:hover { + background: rgba(147, 51, 234, 0.5); +} + +.uuid-section-title { + font-size: 1.25rem; + font-weight: 600; + color: white; + margin: 0 0 1rem 0; + display: flex; + align-items: center; + gap: 0.5rem; + font-family: 'Space Grotesk', sans-serif; +} + +.uuid-current-section, +.uuid-list-section, +.uuid-custom-section { + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 1.5rem; +} + +.uuid-current-display { + display: flex; + align-items: stretch; + gap: 0.75rem; +} + +.uuid-display-input { + flex: 1; + padding: 0.875rem 1rem; + background: rgba(0, 0, 0, 0.4); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + color: rgba(255, 255, 255, 0.9); + font-family: 'JetBrains Mono', monospace; + font-size: 0.875rem; + letter-spacing: 0.5px; +} + +.uuid-action-btn { + padding: 0.875rem 1rem; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + min-width: 44px; +} + +.uuid-action-btn:hover { + background: rgba(147, 51, 234, 0.2); + border-color: rgba(147, 51, 234, 0.4); + color: #9333ea; + transform: translateY(-2px); +} + +.uuid-list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.uuid-generate-btn { + padding: 0.75rem 1.25rem; + background: linear-gradient(135deg, #9333ea, #3b82f6); + border: none; + border-radius: 8px; + color: white; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.uuid-generate-btn:hover { + background: linear-gradient(135deg, #a855f7, #60a5fa); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(147, 51, 234, 0.3); +} + +.uuid-list { + display: flex; + flex-direction: column; + gap: 0.75rem; + max-height: 300px; + overflow-y: auto; +} + +.uuid-list::-webkit-scrollbar { + width: 6px; +} + +.uuid-list::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.2); + border-radius: 3px; +} + +.uuid-list::-webkit-scrollbar-thumb { + background: rgba(147, 51, 234, 0.3); + border-radius: 3px; +} + +.uuid-list-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + transition: all 0.3s ease; +} + +.uuid-list-item:hover { + border-color: rgba(147, 51, 234, 0.3); + background: rgba(147, 51, 234, 0.05); +} + +.uuid-list-item.current { + border-color: rgba(34, 197, 94, 0.4); + background: rgba(34, 197, 94, 0.1); +} + +.uuid-item-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.uuid-item-username { + font-size: 0.95rem; + font-weight: 600; + color: white; +} + +.uuid-item-uuid { + font-size: 0.8rem; + font-family: 'JetBrains Mono', monospace; + color: rgba(255, 255, 255, 0.6); + letter-spacing: 0.5px; +} + +.uuid-item-actions { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.uuid-item-current-badge { + padding: 0.25rem 0.75rem; + background: rgba(34, 197, 94, 0.2); + border: 1px solid rgba(34, 197, 94, 0.3); + border-radius: 12px; + color: #22c55e; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.uuid-custom-form { + display: flex; + align-items: stretch; + gap: 0.75rem; +} + +.uuid-set-btn { + padding: 0.875rem 1.5rem; + background: linear-gradient(135deg, #22c55e, #16a34a); + border: none; + border-radius: 8px; + color: white; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; +} + +.uuid-set-btn:hover { + background: linear-gradient(135deg, #16a34a, #15803d); + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(34, 197, 94, 0.3); +} + +.uuid-custom-hint { + margin-top: 1rem; + font-size: 0.85rem; + color: rgba(249, 115, 22, 0.9); + display: flex; + align-items: center; + gap: 0.5rem; +} + +.uuid-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 2rem; + color: rgba(255, 255, 255, 0.6); + font-size: 0.95rem; +} + +.uuid-loading i { + font-size: 1.25rem; + color: #9333ea; +} + +.uuid-item-btn { + padding: 0.5rem; + 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.6); + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.875rem; + display: flex; + align-items: center; + justify-content: center; + min-width: 32px; + height: 32px; +} + +.uuid-item-btn:hover { + background: rgba(147, 51, 234, 0.2); + border-color: rgba(147, 51, 234, 0.4); + color: #9333ea; +} + +.uuid-item-btn.copy:hover { + background: rgba(34, 197, 94, 0.2); + border-color: rgba(34, 197, 94, 0.4); + color: #22c55e; +} + +.uuid-item-btn.delete:hover { + background: rgba(239, 68, 68, 0.2); + border-color: rgba(239, 68, 68, 0.4); + color: #ef4444; +} + +@media (max-width: 600px) { + .uuid-modal-content { + width: 95vw; + max-height: 90vh; + } + + .uuid-modal-body { + padding: 1rem; + gap: 1.5rem; + } + + .uuid-current-display, + .uuid-custom-form { + flex-direction: column; + } + + .uuid-list-header { + flex-direction: column; + align-items: stretch; + gap: 1rem; + } + + .uuid-list-item { + flex-direction: column; + align-items: stretch; + gap: 1rem; + } + + .uuid-item-actions { + justify-content: center; + } +} + +/* Chat Badges et Animations */ +@keyframes rainbow { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +@keyframes contributorGlow { + 0%, 100% { + background-position: 0% 50%; + filter: brightness(1); + } + 50% { + background-position: 100% 50%; + filter: brightness(1.2); + } +} + +.user-badge { + display: inline-block; + font-size: 0.75rem; + font-weight: bold; + margin-right: 0.25rem; +} + +.message-username { + font-weight: bold; +} + +/* Styles pour le sélecteur de couleur dans le chat */ +.chat-header-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +.chat-color-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: linear-gradient(135deg, #3b82f6, #06b6d4); + border: 1px solid rgba(59, 130, 246, 0.6); + border-radius: 8px; + color: white; + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.875rem; + font-weight: 600; + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3); +} + +.chat-color-btn:hover { + background: linear-gradient(135deg, #2563eb, #0891b2); + border-color: rgba(37, 99, 235, 0.8); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4); +} + +/* Modal de sélection de couleur */ +.chat-color-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(10px); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + animation: fadeIn 0.3s ease; +} + +.chat-color-modal-content { + background: rgba(20, 20, 20, 0.95); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 16px; + width: 90%; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + animation: slideUp 0.3s ease; +} + +.chat-color-modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 2rem 2rem 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.chat-color-modal-title { + font-size: 1.5rem; + font-weight: 700; + color: white; + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0; +} + +.chat-color-modal-title i { + color: #9333ea; +} + +.modal-close-btn { + padding: 0.5rem; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + transition: all 0.3s ease; +} + +.modal-close-btn:hover { + background: rgba(239, 68, 68, 0.2); + border-color: rgba(239, 68, 68, 0.4); + color: #ef4444; +} + +.chat-color-modal-body { + padding: 2rem; +} + +.color-type-selector { + display: flex; + gap: 1rem; + margin-bottom: 2rem; +} + +.color-type-option { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + font-weight: 500; + color: rgba(255, 255, 255, 0.8); +} + +.color-type-option input[type="radio"] { + display: none; +} + +.radio-custom { + width: 20px; + height: 20px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + position: relative; + transition: all 0.3s ease; +} + +.color-type-option input[type="radio"]:checked + .radio-custom { + border-color: #9333ea; + background: rgba(147, 51, 234, 0.2); +} + +.color-type-option input[type="radio"]:checked + .radio-custom::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 10px; + height: 10px; + background: #9333ea; + border-radius: 50%; +} + +.color-section { + margin-bottom: 2rem; +} + +.color-section h4 { + font-size: 1.1rem; + font-weight: 600; + color: white; + margin-bottom: 1rem; +} + +.predefined-colors { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 0.5rem; + margin-bottom: 1rem; +} + +.color-option { + width: 40px; + height: 40px; + border-radius: 8px; + cursor: pointer; + border: 2px solid rgba(255, 255, 255, 0.2); + transition: all 0.3s ease; +} + +.color-option:hover, +.color-option.selected { + transform: scale(1.1); + border-color: white; + box-shadow: 0 0 15px rgba(255, 255, 255, 0.5); +} + +.custom-color-input, +.gradient-color-input { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; +} + +.custom-color-input label, +.gradient-color-input label { + font-weight: 500; + color: rgba(255, 255, 255, 0.8); + min-width: 80px; +} + +.custom-color-input input[type="color"], +.gradient-color-input input[type="color"] { + width: 60px; + height: 40px; + border: none; + border-radius: 8px; + cursor: pointer; + background: transparent; +} + +.gradient-controls { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.gradient-direction { + display: flex; + align-items: center; + gap: 1rem; +} + +.gradient-direction label { + font-weight: 500; + color: rgba(255, 255, 255, 0.8); + min-width: 80px; +} + +.gradient-direction select { + padding: 0.5rem; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + color: white; + cursor: pointer; +} + +.color-preview { + margin-top: 2rem; + padding: 1rem; + background: rgba(255, 255, 255, 0.03); + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.preview-username { + font-size: 1.2rem; + font-weight: bold; + text-align: center; + padding: 1rem; + background: rgba(0, 0, 0, 0.3); + border-radius: 8px; +} + +.chat-color-modal-footer { + padding: 1rem 2rem 2rem; + display: flex; + gap: 1rem; + justify-content: flex-end; +} + +.btn-secondary, +.btn-primary { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 12px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; +} + +.btn-secondary { + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.7); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.btn-secondary:hover { + background: rgba(255, 255, 255, 0.1); + color: white; +} + +.btn-primary { + background: linear-gradient(135deg, #9333ea, #3b82f6); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 16px rgba(147, 51, 234, 0.4); +} +.color-picker-container { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 1rem; +} + +.color-option { + width: 30px; + height: 30px; + border-radius: 50%; + border: 2px solid transparent; + cursor: pointer; + transition: all 0.2s ease; +} + +.color-option:hover { + transform: scale(1.1); +} + +.color-option.selected { + border-color: #fff; + box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); +} diff --git a/README.md b/README.md index eced181..1d0e3d5 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,19 @@ -# 🎮 Hytale F2P Launcher | Multiplayer Support +# 🎮 Hytale F2P Launcher | Multiplayer Support [Windows, MacOS, Linux]
-![Version](https://img.shields.io/badge/Version-2.0.1-green?style=for-the-badge) +![Version](https://img.shields.io/badge/Version-2.0.2-green?style=for-the-badge) ![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey?style=for-the-badge) ![License](https://img.shields.io/badge/License-Educational-blue?style=for-the-badge) -**A modern, cross-platform offline launcher for Hytale with automatic updates and multiplayer support (windows users & non-premium only)** +**A modern, cross-platform offline launcher for Hytale with automatic updates and multiplayer support (all OS supported)** [![GitHub stars](https://img.shields.io/github/stars/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/stargazers) [![GitHub forks](https://img.shields.io/github/forks/amiayweb/Hytale-F2P?style=social)](https://github.com/amiayweb/Hytale-F2P/network/members) ⭐ **If you find this project useful, please give it a star!** ⭐ -🛑 **Found a problem? Open an issue! I’m on Windows, so I can’t test on macOS or Linux.** 🛑 + +🛑 **Found a problem? Join the Discord: https://discord.gg/gME8rUy3MB** 🛑
@@ -36,7 +37,7 @@ - 💾 **Data Preservation** - Intelligent UserData backup and restoration during updates - 🌐 **Cross-Platform** - Full support for Windows, Linux (X11/Wayland), and macOS - ☕ **Java Management** - Automatic Java runtime detection and installation -- 🎮 **Multiplayer Support** - Automatic multiplayer client installation (Windows) +- 🎮 **Multiplayer Support** - Automatic multiplayer client installation (Windows, macOS & Linux !) 🛡️ **Advanced Features** - 📁 **Custom Installation** - Choose your own installation directory @@ -63,7 +64,7 @@ See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https #### macOS See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases) section. -#### 🖥️ How to create server (Windows Only)? +#### 🖥️ How to play online on F2P? See [SERVER.md](SERVER.md) @@ -77,7 +78,15 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions. ## 📋 Changelog -### 🆕 v2.0.1 *(Latest)* +### 🆕 v2.0.2 *(Latest)* +- 🎮 **Discord RPC Integration** - Added Discord Rich Presence with toggle in settings (enabled by default) +- 🌐 **Cross-Platform Multiplayer** - Added multiplayer patch support for Windows, Linux, and macOS +- 🎨 **Chat Improvements** - Simplified chat color system +- 🏆 **Badge System Expansion** - Added new FOUNDER UUID to the badge system +- 🔧 **Progress Bar Fix** - Resolved issue where download progress bar stayed active after game launch +- 🐛 **Bug Fixes**: General fixes + +### 🔄 v2.0.1 - 📊 **Advanced Logging System** - Complete logging with timestamps, file rotation, and session tracking - 🔧 **Play Button Fix** - Resolved issue where play button could get stuck in "CHECKING..." state - 💬 **Discord Integration** - Added closable Discord notification for community engagement @@ -129,14 +138,16 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions. ### 🏆 Project Creator - [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator* +- [**@Relyz1993**](https://github.com/Relyz1993) - *Server Helper & Second Developer & Project Creator* ### 🌟 Contributors +- [**@sanasol**](https://github.com/sanasol) - *Main Issues fixer | Multiplayer Patcher* +- [**@Terromur**](https://github.com/Terromur) - *Main Issues fixer | Beta tester* +- [**@fazrigading**](https://github.com/fazrigading) - *Main Issues fixer | Beta tester* +- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester* - [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer* - [**@crimera**](https://github.com/crimera) - *Issues fixer* -- [**@sanasol**](https://github.com/sanasol) - *Issues fixer* -- [**@Terromur**](https://github.com/Terromur) - *Issues fixer* - [**@Citeli-py**](https://github.com/Citeli-py) - *Issues fixer* -- [**@ericiskoolbeans**](https://github.com/ericiskoolbeans) - *Beta Tester* --- @@ -156,10 +167,7 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
-[![GitHub Issues](https://img.shields.io/badge/GitHub-Issues-red?style=for-the-badge&logo=github)](https://github.com/amiayweb/Hytale-F2P/issues) -[![Discussions](https://img.shields.io/badge/GitHub-Discussions-blue?style=for-the-badge&logo=github)](https://github.com/amiayweb/Hytale-F2P/discussions) - -**Need help?** Open an [issue](https://github.com/amiayweb/Hytale-F2P/issues) or start a [discussion](https://github.com/amiayweb/Hytale-F2P/discussions)! +**Need help?** Join us: https://discord.gg/gME8rUy3MB
diff --git a/SERVER.md b/SERVER.md index bde93b6..0b3e086 100644 --- a/SERVER.md +++ b/SERVER.md @@ -1,87 +1,444 @@ -# Hytale F2P Server Setup Guide +# Hytale F2P Server Guide -## Server File Setup +Play with friends online! This guide covers both easy in-game hosting and advanced dedicated server setup. -**Download server file:** -``` -https://files.hytalef2p.com/server -``` - -**Replace the file here:** -`\HytaleF2P\release\package\game\latest\Server` - -If you don't have any custom installation path: - -1. Press **WIN + R** -2. Type: `%localappdata%\HytaleF2P\release\package\game\latest\Server` -3. Press **Enter** - -You will be redirected to the correct folder automatically. - -## Network Setup - Radmin VPN Required - -**Important:** The server only supports third-party software for LAN-style connections. You must use **Radmin VPN** to connect players together. - -1. **Download and install [Radmin VPN](https://www.radmin-vpn.com/)** -2. **Create or join a network** in Radmin VPN -3. **All players must be connected** to the same Radmin network -4. **Use the Radmin VPN IP address** to connect to the server - -This creates a virtual LAN environment that allows the Hytale server to work properly with multiple players. - -## RAM Allocation Guide (Windows) - -When you start a Hytale server using `start-server.bat`, Java will use very little memory by default. -This can cause slow startup, crashes, or the server not launching at all. - -**You should always allocate RAM in your launch command.** - -Edit your `start-server.bat` file and use the version that matches your PC: +DOWNLOAD SERVER FILES HERE: https://discord.gg/MEyWUxt77m --- -### PC with 4 GB RAM -*Best for small servers / testing* +## Part 1: Playing with Friends (Online Play) + +The easiest way to play with friends - no manual server setup required! + +### How It Works + +1. **Start the game** via F2P Launcher +2. **Click "Online Play"** in the main menu +3. **Share the invite code** with your friends +4. Friends enter your invite code to join + +The game automatically handles networking using UPnP/STUN/NAT traversal. + +### Network Requirements + +For Online Play to work, you need: + +- **UPnP enabled** on your router (most routers have this on by default) +- **Public IP address** from your ISP (not behind CGNAT) + +### Common Issues + +#### "NAT Type: Carrier-Grade NAT (CGNAT)" Warning + +If you see this message: +``` +Connected via UPnP +NAT Type: Carrier-Grade NAT (CGNAT) +Warning: Your network configuration may prevent other players from connecting. +``` + +**What this means:** Your ISP doesn't give you a public IP address. Multiple customers share one public IP, which blocks incoming connections. + +**Solutions:** + +1. **Contact your ISP** - Request a public/static IP address (may cost extra) +2. **Use a VPN with port forwarding** - Services like Mullvad, PIA, or AirVPN offer this +3. **Use Radmin VPN or Playit.gg** - Create a virtual LAN with friends (see below) +4. **Have a friend with public IP host instead** + +#### "UPnP Failed" or "Port Mapping Failed" + +**Check your router:** +1. Log into router admin panel (usually `192.168.1.1` or `192.168.0.1`) +2. Find UPnP settings (often under "Advanced" or "NAT") +3. Enable UPnP if disabled +4. Restart your router + +**If UPnP isn't available:** +- Manually forward **port 5520 UDP** to your computer's local IP +- See "Port Forwarding" section below + +#### "Strict NAT" or "Symmetric NAT" + +Some routers have restrictive NAT that blocks peer connections. + +**Try:** +1. Enable "NAT Passthrough" or "NAT Filtering: Open" in router settings +2. Put your device in router's DMZ (temporary test only) +3. Use Radmin VPN as workaround + +### Workarounds for NAT/CGNAT Issues + +#### Option 1: playit.gg (Recommended) + +Free tunneling service - only the host needs to install it: + +1. **Download [playit.gg](https://playit.gg/)** and run it - Connect your account from the terminal (do not close it when playing on the server) +2. **Add a tunnel** - Select "UDP", tunnel description of "Hytale Server", port count `1`, and local port `5520` +3. **Start the tunnel** - You'll get a public address like `xx-xx.gl.at.ply.gg:5520` +4. **Share the address** - Friends connect directly using this address + +Works with both Online Play and dedicated servers. No software needed for players joining. + +#### Option 2: Radmin VPN + +Creates a virtual LAN - all players need to install it: + +1. **Download [Radmin VPN](https://www.radmin-vpn.com/)** - All players install it +2. **Create a network** - One person creates, others join with network name/password +3. **Host via Online Play** - Use your Radmin VPN IP instead +4. **Friends connect** - They'll see you on the virtual LAN + +Both options bypass all NAT/CGNAT issues. + +--- + +## Part 2: Dedicated Server (Advanced) + +For 24/7 servers, custom configurations, or hosting on a VPS/dedicated machine. + +### Quick Start + +#### Step 1: Get the Server JAR + +The server scripts will automatically download the pre-patched server JAR if it's not present. + +**Option A:** Let the scripts download automatically (requires `HYTALE_SERVER_URL` to be configured) + +**Option B:** Manually place `HytaleServer.jar` (pre-patched for F2P) in the Server directory: + +- **Windows:** `%localappdata%\HytaleF2P\release\package\game\latest\Server` +- **macOS:** `~/Library/Application Support/HytaleF2P/release/package/game/latest/Server` +- **Linux:** `~/.hytalef2p/release/package/game/latest/Server` + +If you have a custom install path, the Server folder is inside your custom location under `HytaleF2P/release/package/game/latest/Server`. + +#### Step 2: Run the Server + +**Windows:** +```batch +cd scripts +run_server.bat +``` + +**macOS / Linux:** +```bash +cd scripts +./run_server.sh +``` + +The scripts will: +1. Find your game installation automatically +2. Download the pre-patched server JAR if needed +3. Fetch session tokens from the auth server +4. Start the server + +#### Step 3: Connect Players + +Share your server IP address with players. They connect via the F2P Launcher's server browser or direct connect. + +--- + +## Network Setup (Dedicated Server) + +### Local Network (LAN) + +If all players are on the same network: +1. Find your local IP: `ipconfig` (Windows) or `ifconfig` (Mac/Linux) +2. Share this IP with players on your network +3. Default port is `5520` + +### Port Forwarding (Internet Play) + +To allow direct internet connections: + +1. Forward **port 5520 (UDP)** in your router +2. Find your public IP at [whatismyip.com](https://whatismyip.com) +3. Share your public IP with players + +**Windows Firewall:** +```powershell +# Run as Administrator +netsh advfirewall firewall add rule name="Hytale Server" dir=in action=allow protocol=UDP localport=5520 +``` + +--- + +## Configuration + +### Environment Variables + +Set these before running to customize your server: + +| Variable | Default | Description | +|----------|---------|-------------| +| `HYTALE_SERVER_URL` | (placeholder) | URL to download pre-patched server JAR | +| `HYTALE_AUTH_DOMAIN` | `sanasol.ws` | Auth server domain | +| `HYTALE_BIND` | `0.0.0.0:5520` | Server IP and port | +| `HYTALE_AUTH_MODE` | `authenticated` | Auth mode (see below) | +| `HYTALE_SERVER_NAME` | `My Hytale Server` | Server display name | +| `HYTALE_GAME_PATH` | (auto-detected) | Override game location | +| `JVM_XMS` | `2G` | Minimum Java memory | +| `JVM_XMX` | `4G` | Maximum Java memory | + +**Example (Windows):** +```batch +set HYTALE_SERVER_NAME=My Awesome Server +set JVM_XMX=8G +run_server.bat +``` + +**Example (Linux/macOS):** +```bash +HYTALE_SERVER_NAME="My Awesome Server" JVM_XMX=8G ./run_server.sh +``` + +### Authentication Modes + +| Mode | Description | Use Case | +|------|-------------|----------| +| `authenticated` | Players log in via F2P Launcher | Public servers | +| `unauthenticated` | No login required | LAN parties, testing | +| `singleplayer` | Local play only | Solo testing | + +--- + +## RAM Allocation Guide + +Adjust memory based on your system: + +| System RAM | Players | JVM_XMS | JVM_XMX | +|------------|---------|---------|---------| +| 4 GB | 1-3 | `512M` | `2G` | +| 8 GB | 3-8 | `1G` | `4G` | +| 16 GB | 8-15 | `2G` | `8G` | +| 32 GB | 15+ | `4G` | `12G` | + +**Example for large server:** +```bash +JVM_XMS=4G JVM_XMX=12G ./run_server.sh +``` + +**Tips:** +- `-Xms` = minimum RAM (allocated at startup) +- `-Xmx` = maximum RAM (upper limit) +- Never allocate all your system RAM - leave room for OS +- Start conservative and increase if needed + +--- + +## Server Commands + +Once running, use these commands in the console: + +| Command | Description | +|---------|-------------| +| `help` | Show all commands | +| `stop` | Stop server gracefully | +| `save` | Force world save | +| `list` | List online players | +| `op ` | Give operator status | +| `deop ` | Remove operator status | +| `kick ` | Kick a player | +| `ban ` | Ban a player | +| `unban ` | Unban a player | +| `tp ` | Teleport player | + +--- + +## Command Line Options + +Pass these when starting the server: ```bash -java -Xms512M -Xmx2G -jar HytaleServer.jar --assets ..\Assets.zip +./run_server.sh [OPTIONS] ``` -- Uses up to **2 GB** -- Leaves enough memory for Windows +| Option | Description | +|--------|-------------| +| `--bind ` | Server address (default: 0.0.0.0:5520) | +| `--auth-mode ` | Authentication mode | +| `--universe ` | Path to world data | +| `--mods ` | Path to mods folder | +| `--backup` | Enable automatic backups | +| `--backup-dir ` | Backup directory | +| `--backup-frequency ` | Backup interval | +| `--owner-name ` | Server owner username | +| `--allow-op` | Allow op commands | +| `--disable-sentry` | Disable crash reporting | +| `--help` | Show all options | + +**Example:** +```bash +./run_server.sh --backup --backup-frequency 30 --allow-op +``` --- -### PC with 8 GB RAM -*Good for small communities* +## File Structure + +``` +/ +├── Assets.zip # Game assets (required) +├── Client/ # Game client +└── Server/ + ├── HytaleServer.jar # Server executable (pre-patched) + ├── HytaleServer.aot # AOT cache (faster startup) + ├── universe/ # World data + │ ├── world/ # Default world + │ └── players/ # Player data + ├── mods/ # Server mods (optional) + └── Licenses/ # License files +``` + +--- + +## Backups + +### Automatic Backups ```bash -java -Xms1G -Xmx4G -jar HytaleServer.jar --assets ..\Assets.zip +./run_server.sh --backup --backup-dir ./backups --backup-frequency 30 ``` -- Uses up to **4 GB** -- Stable for most setups +### Manual Backup + +1. Use `save` command or stop the server +2. Copy the `universe/` folder +3. Store in a safe location + +### Restore + +1. Stop the server +2. Delete/rename current `universe/` +3. Copy backup to `universe/` +4. Restart server --- -### PC with 16 GB RAM -*Perfect for large or modded servers* +## Troubleshooting + +### "Java not found" or "Java version too old" + +**Java 21 is REQUIRED** (the server uses class file version 65.0). + +**Install Java 21:** +- **Windows:** `winget install EclipseAdoptium.Temurin.21.JDK` +- **macOS:** `brew install openjdk@21` +- **Ubuntu:** `sudo apt install openjdk-21-jdk` +- **Fedora:** `sudo dnf install java-21-openjdk` +- **Arch:** `sudo pacman -S jdk21-openjdk` +- **Download:** [adoptium.net/temurin/releases/?version=21](https://adoptium.net/temurin/releases/?version=21) + +**macOS: Set Java 21 as default:** +```bash +export JAVA_HOME=$(/usr/libexec/java_home -v 21) +export PATH="$JAVA_HOME/bin:$PATH" +``` +Add these lines to `~/.zshrc` or `~/.bash_profile` to make permanent. + +### "Game directory not found" + +- Download game via F2P Launcher first +- Or set `HYTALE_GAME_PATH` environment variable +- Check custom install path in launcher settings + +### "Assets.zip not found" + +Game files incomplete. Re-download via the launcher. + +### "Port already in use" ```bash -java -Xms2G -Xmx8G -jar HytaleServer.jar --assets ..\Assets.zip +./run_server.sh --bind 0.0.0.0:5521 ``` -- Uses up to **8 GB** -- Ideal for heavy worlds and plugins +### "Out of memory" + +Increase JVM_XMX: +```bash +JVM_XMX=6G ./run_server.sh +``` + +### Players can't connect + +1. Server shows "Server Ready"? +2. Using F2P Launcher (not official)? +3. Port 5520 open in firewall? +4. Port forwarding configured (for internet)? +5. Try `--auth-mode unauthenticated` for testing + +### "Authentication failed" + +- Ensure players use F2P Launcher +- Auth server may be temporarily down +- Test with `--auth-mode unauthenticated` --- -## Tips +## Docker Deployment (Advanced) -- `-Xms` = minimum RAM allocation -- `-Xmx` = maximum RAM allocation -- **Never allocate all your system RAM** — Windows still needs memory to run -- **Test your configuration** with a small world first -- **Monitor server performance** and adjust RAM as needed +For production servers, use Docker: +```bash +docker run -d \ + --name hytale-server \ + -p 5520:5520/udp \ + -v ./data:/data \ + -e HYTALE_AUTH_DOMAIN=sanasol.ws \ + -e HYTALE_SERVER_NAME="My Server" \ + -e JVM_XMX=8G \ + ghcr.io/hybrowse/hytale-server-docker:latest +``` +See [Docker documentation](https://github.com/Hybrowse/hytale-server-docker) for details. + +--- + +## Server Settings Summary + +### Minimal Setup +```bash +./run_server.sh +``` + +### Custom Memory +```bash +JVM_XMS=2G JVM_XMX=8G ./run_server.sh +``` + +### Custom Port +```bash +HYTALE_BIND=0.0.0.0:25565 ./run_server.sh +``` + +### LAN Party (No Auth) +```bash +./run_server.sh --auth-mode unauthenticated +``` + +### Full Custom Setup +```bash +HYTALE_SERVER_NAME="Epic Server" \ +HYTALE_BIND=0.0.0.0:5520 \ +JVM_XMS=2G \ +JVM_XMX=8G \ +./run_server.sh --backup --backup-frequency 15 --allow-op +``` + +--- + +## Getting Help + +- Check server console logs for errors +- Test with `--auth-mode unauthenticated` first +- Ensure all players have F2P Launcher +- Join the community for support + +--- + +## Credits + +- Hytale F2P Project +- [Hybrowse Docker Image](https://github.com/Hybrowse/hytale-server-docker) +- Auth Server: sanasol.ws diff --git a/backend/core/config.js b/backend/core/config.js index 9857b70..80d31cb 100644 --- a/backend/core/config.js +++ b/backend/core/config.js @@ -2,6 +2,36 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); + +// Default auth domain - can be overridden by env var or config +const DEFAULT_AUTH_DOMAIN = 'sanasol.ws'; + +// Get auth domain from env, config, or default +function getAuthDomain() { + // First check environment variable + if (process.env.HYTALE_AUTH_DOMAIN) { + return process.env.HYTALE_AUTH_DOMAIN; + } + // Then check config file + const config = loadConfig(); + if (config.authDomain) { + return config.authDomain; + } + // Fall back to default + return DEFAULT_AUTH_DOMAIN; +} + +// Get full auth server URL +function getAuthServerUrl() { + const domain = getAuthDomain(); + return `https://sessions.${domain}`; +} + +// Save auth domain to config +function saveAuthDomain(domain) { + saveConfig({ authDomain: domain || DEFAULT_AUTH_DOMAIN }); +} + function getAppDir() { const home = os.homedir(); if (process.platform === 'win32') { @@ -94,6 +124,15 @@ function loadInstallPath() { return config.installPath || ''; } +function saveDiscordRPC(enabled) { + saveConfig({ discordRPC: !!enabled }); +} + +function loadDiscordRPC() { + const config = loadConfig(); + return config.discordRPC !== undefined ? config.discordRPC : true; +} + function saveModsToConfig(mods) { try { let config = loadConfig(); @@ -143,6 +182,70 @@ function markAsLaunched() { saveConfig({ hasLaunchedBefore: true, firstLaunchDate: new Date().toISOString() }); } +// UUID Management Functions +function getCurrentUuid() { + const username = loadUsername(); + return getUuidForUser(username); +} + +function getAllUuidMappings() { + const config = loadConfig(); + return config.userUuids || {}; +} + +function setUuidForUser(username, uuid) { + const { v4: uuidv4, validate: validateUuid } = require('uuid'); + + // Validate UUID format + if (!validateUuid(uuid)) { + throw new Error('Invalid UUID format'); + } + + const config = loadConfig(); + const userUuids = config.userUuids || {}; + userUuids[username] = uuid; + saveConfig({ userUuids }); + + return uuid; +} + +function generateNewUuid() { + const { v4: uuidv4 } = require('uuid'); + return uuidv4(); +} + +function deleteUuidForUser(username) { + const config = loadConfig(); + const userUuids = config.userUuids || {}; + + if (userUuids[username]) { + delete userUuids[username]; + saveConfig({ userUuids }); + return true; + } + + return false; +} + +function resetCurrentUserUuid() { + const username = loadUsername(); + const { v4: uuidv4 } = require('uuid'); + const newUuid = uuidv4(); + + return setUuidForUser(username, newUuid); +} + +function saveChatColor(color) { + const config = loadConfig(); + config.chatColor = color; + saveConfig(config); +} + +function loadChatColor() { + const config = loadConfig(); + return config.chatColor || '#3498db'; +} + module.exports = { loadConfig, saveConfig, @@ -150,14 +253,29 @@ module.exports = { loadUsername, saveChatUsername, loadChatUsername, + saveChatColor, + loadChatColor, getUuidForUser, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, + saveDiscordRPC, + loadDiscordRPC, saveModsToConfig, loadModsFromConfig, isFirstLaunch, markAsLaunched, - CONFIG_FILE + CONFIG_FILE, + // Auth server exports + getAuthServerUrl, + getAuthDomain, + saveAuthDomain, + // UUID Management exports + getCurrentUuid, + getAllUuidMappings, + setUuidForUser, + generateNewUuid, + deleteUuidForUser, + resetCurrentUserUuid }; diff --git a/backend/launcher.js b/backend/launcher.js index e7deed7..405fba6 100644 --- a/backend/launcher.js +++ b/backend/launcher.js @@ -7,16 +7,27 @@ const { loadUsername, saveChatUsername, loadChatUsername, + saveChatColor, + loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, + saveDiscordRPC, + loadDiscordRPC, saveModsToConfig, loadModsFromConfig, getUuidForUser, isFirstLaunch, markAsLaunched, - CONFIG_FILE + CONFIG_FILE, + // UUID Management + getCurrentUuid, + getAllUuidMappings, + setUuidForUser, + generateNewUuid, + deleteUuidForUser, + resetCurrentUserUuid } = require('./core/config'); const { getResolvedAppDir, getModsPath } = require('./core/paths'); @@ -37,8 +48,6 @@ const { const { getJavaDetection } = require('./managers/javaManager'); -const { checkAndInstallMultiClient } = require('./managers/multiClientManager'); - const { downloadAndReplaceHomePageUI, findHomePageUIPath, @@ -85,6 +94,8 @@ module.exports = { loadUsername, saveChatUsername, loadChatUsername, + saveChatColor, + loadChatColor, getUuidForUser, // Java configuration functions @@ -96,6 +107,10 @@ module.exports = { saveInstallPath, loadInstallPath, + // Discord RPC functions + saveDiscordRPC, + loadDiscordRPC, + // Version functions getInstalledClientVersion, getLatestClientVersion, @@ -106,8 +121,13 @@ module.exports = { // Player ID functions getOrCreatePlayerId, - // Multi-client functions - checkAndInstallMultiClient, + // UUID Management functions + getCurrentUuid, + getAllUuidMappings, + setUuidForUser, + generateNewUuid, + deleteUuidForUser, + resetCurrentUserUuid, // Mod management functions getModsPath, diff --git a/backend/managers/gameLauncher.js b/backend/managers/gameLauncher.js index 60c32ba..4d9e300 100644 --- a/backend/managers/gameLauncher.js +++ b/backend/managers/gameLauncher.js @@ -1,17 +1,105 @@ const fs = require('fs'); const path = require('path'); +const crypto = require('crypto'); const { exec } = require('child_process'); 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 { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser } = require('../core/config'); +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'); const { updateGameFiles } = require('./gameManager'); +// Client patcher for custom auth server (sanasol.ws) +let clientPatcher = null; +try { + clientPatcher = require('../utils/clientPatcher'); +} catch (err) { + console.log('[Launcher] Client patcher not available:', err.message); +} + const execAsync = promisify(exec); +// Fetch tokens from the auth server (properly signed with server's Ed25519 key) +async function fetchAuthTokens(uuid, name) { + const authServerUrl = getAuthServerUrl(); + try { + console.log(`Fetching auth tokens from ${authServerUrl}/game-session/child`); + + const response = await fetch(`${authServerUrl}/game-session/child`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + uuid: uuid, + name: name, + scopes: ['hytale:server', 'hytale:client'] + }) + }); + + if (!response.ok) { + throw new Error(`Auth server returned ${response.status}`); + } + + const data = await response.json(); + console.log('Auth tokens received from server'); + + return { + identityToken: data.IdentityToken || data.identityToken, + sessionToken: data.SessionToken || data.sessionToken + }; + } catch (error) { + console.error('Failed to fetch auth tokens:', error.message); + // Fallback to local generation if server unavailable + return generateLocalTokens(uuid, name); + } +} + +// Fallback: Generate tokens locally (won't pass signature validation but allows offline testing) +function generateLocalTokens(uuid, name) { + console.log('Using locally generated tokens (fallback mode)'); + const authServerUrl = getAuthServerUrl(); + const now = Math.floor(Date.now() / 1000); + const exp = now + 36000; + + const header = Buffer.from(JSON.stringify({ + alg: 'EdDSA', + kid: '2025-10-01', + typ: 'JWT' + })).toString('base64url'); + + const identityPayload = Buffer.from(JSON.stringify({ + sub: uuid, + name: name, + username: name, + entitlements: ['game.base'], + scope: 'hytale:server hytale:client', + iat: now, + exp: exp, + iss: authServerUrl, + jti: uuidv4() + })).toString('base64url'); + + const sessionPayload = Buffer.from(JSON.stringify({ + sub: uuid, + scope: 'hytale:server', + iat: now, + exp: exp, + iss: authServerUrl, + jti: uuidv4() + })).toString('base64url'); + + const signature = crypto.randomBytes(64).toString('base64url'); + + return { + identityToken: `${header}.${identityPayload}.${signature}`, + sessionToken: `${header}.${sessionPayload}.${signature}` + }; +} + async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) { const customAppDir = getResolvedAppDir(installPathOverride); const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest'); @@ -53,6 +141,51 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr } } + const uuid = getUuidForUser(playerName); + + // Fetch tokens from auth server + if (progressCallback) { + progressCallback('Fetching authentication tokens...', null, null, null, null); + } + const { identityToken, sessionToken } = await fetchAuthTokens(uuid, playerName); + + // Patch client and server binaries to use custom auth server (BEFORE signing on macOS) + const authDomain = getAuthDomain(); + if (clientPatcher) { + try { + if (progressCallback) { + progressCallback('Patching game for custom server...', null, null, null, null); + } + console.log(`Patching game binaries for ${authDomain}...`); + + const patchResult = await clientPatcher.ensureClientPatched(gameLatest, (msg, percent) => { + console.log(`[Patcher] ${msg}`); + if (progressCallback && msg) { + progressCallback(msg, percent, null, null, null); + } + }); + + if (patchResult.success) { + if (patchResult.alreadyPatched) { + console.log(`Game already patched for ${authDomain}`); + } else { + console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`); + if (patchResult.client) { + console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`); + } + if (patchResult.server) { + console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`); + } + } + } else { + console.warn('Game patching failed:', patchResult.error); + } + } catch (patchError) { + console.warn('Game patching failed (game may not connect to custom server):', patchError.message); + } + } + + // macOS: Sign binaries AFTER patching so the patched binaries have valid signatures if (process.platform === 'darwin') { try { const appBundle = path.join(gameLatest, 'Client', 'Hytale.app'); @@ -66,10 +199,10 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr if (fs.existsSync(appBundle)) { await signPath(appBundle, true); - console.log('Signed macOS app bundle'); + console.log('Signed macOS app bundle (after patching)'); } else { await signPath(path.dirname(clientPath), true); - console.log('Signed macOS client binary'); + console.log('Signed macOS client binary (after patching)'); } if (javaBin && fs.existsSync(javaBin)) { @@ -85,7 +218,7 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr if (fs.existsSync(serverDir)) { await execAsync(`xattr -cr "${serverDir}"`).catch(() => {}); await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => {}); - console.log('Signed server binaries'); + console.log('Signed server binaries (after patching)'); } if (javaBin && fs.existsSync(javaBin)) { @@ -113,13 +246,14 @@ exec "$REAL_JAVA" "\${ARGS[@]}" } } - const uuid = getUuidForUser(playerName); const args = [ '--app-dir', gameLatest, '--java-exec', javaBin, - '--auth-mode', 'offline', + '--auth-mode', 'authenticated', '--uuid', uuid, '--name', playerName, + '--identity-token', identityToken, + '--session-token', sessionToken, '--user-dir', userDataDir ]; @@ -269,4 +403,4 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac module.exports = { launchGame, launchGameWithVersionCheck -}; +}; \ No newline at end of file diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index 5b313eb..adc67ef 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -6,7 +6,6 @@ const { getOS, getArch } = require('../utils/platformUtils'); const { downloadFile } = require('../utils/fileManager'); const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager'); const { installButler } = require('./butlerManager'); -const { checkAndInstallMultiClient } = require('./multiClientManager'); const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager'); const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig } = require('../core/config'); const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager'); @@ -165,9 +164,6 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, fs.renameSync(tempUpdateDir, gameDir); - const multiResult = await checkAndInstallMultiClient(gameDir, progressCallback); - console.log('Multiplayer-client check result after update:', multiResult); - const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback); console.log('HomePage.ui update result after update:', homeUIResult); @@ -318,9 +314,6 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir); await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir); - const multiResult = await checkAndInstallMultiClient(customGameDir, progressCallback); - console.log('Multiplayer check result:', multiResult); - const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback); console.log('HomePage.ui update result after installation:', homeUIResult); @@ -334,8 +327,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver return { success: true, - installed: true, - multiClient: multiResult + installed: true }; } diff --git a/backend/managers/multiClientManager.js b/backend/managers/multiClientManager.js index b2baaf6..e69de29 100644 --- a/backend/managers/multiClientManager.js +++ b/backend/managers/multiClientManager.js @@ -1,86 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const { findClientPath } = require('../core/paths'); -const { downloadFile } = require('../utils/fileManager'); -const { getLatestClientVersion, getMultiClientVersion } = require('../services/versionManager'); - -async function downloadMultiClient(gameDir, progressCallback) { - try { - if (process.platform !== 'win32') { - console.log('Multiplayer-client is only available for Windows'); - return { success: false, reason: 'Platform not supported' }; - } - - const clientPath = findClientPath(gameDir); - if (!clientPath) { - throw new Error('Game client not found. Install game first.'); - } - - console.log('Downloading Multiplayer from server...'); - if (progressCallback) { - progressCallback('Downloading Multiplayer...', null, null, null, null); - } - - const clientUrl = 'http://3.10.208.30:3002/client'; - const tempClientPath = path.join(path.dirname(clientPath), 'HytaleClient_temp.exe'); - - await downloadFile(clientUrl, tempClientPath, progressCallback); - - const backupPath = path.join(path.dirname(clientPath), 'HytaleClient_original.exe'); - if (!fs.existsSync(backupPath)) { - fs.copyFileSync(clientPath, backupPath); - console.log('Original client backed up'); - } - - fs.renameSync(tempClientPath, clientPath); - - if (progressCallback) { - progressCallback('Multiplayer installed', 100, null, null, null); - } - console.log('Multiplayer installed successfully'); - - return { success: true, installed: true }; - - } catch (error) { - console.error('Error installing Multiplayer:', error); - throw new Error(`Failed to install Multiplayer: ${error.message}`); - } -} - -async function checkAndInstallMultiClient(gameDir, progressCallback) { - try { - if (process.platform !== 'win32') { - console.log('Multiplayer check skipped (Windows only)'); - return { success: true, skipped: true, reason: 'Windows only' }; - } - - console.log('Checking for Multiplayer availability...'); - - const [clientVersion, multiVersion] = await Promise.all([ - getLatestClientVersion(), - getMultiClientVersion() - ]); - - if (!multiVersion) { - console.log('Multiplayer not available'); - return { success: true, skipped: true, reason: 'Multiplayer not available' }; - } - - if (clientVersion === multiVersion) { - console.log(`Versions match (${clientVersion}), installing Multiplayer...`); - return await downloadMultiClient(gameDir, progressCallback); - } else { - console.log(`Version mismatch: client=${clientVersion}, multi=${multiVersion}`); - return { success: true, skipped: true, reason: 'Version mismatch' }; - } - - } catch (error) { - console.error('Error checking Multiplayer:', error); - return { success: false, error: error.message }; - } -} - -module.exports = { - downloadMultiClient, - checkAndInstallMultiClient -}; diff --git a/backend/managers/uiFileManager.js b/backend/managers/uiFileManager.js index 3a169fb..531ca88 100644 --- a/backend/managers/uiFileManager.js +++ b/backend/managers/uiFileManager.js @@ -10,7 +10,7 @@ async function downloadAndReplaceHomePageUI(gameDir, progressCallback) { progressCallback('Downloading HomePage.ui...', null, null, null, null); } - const homeUIUrl = 'http://3.10.208.30:3002/api/HomeUI'; + const homeUIUrl = 'https://files.hytalef2p.com/api/HomeUI'; const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui'); await downloadFile(homeUIUrl, tempHomePath); @@ -63,7 +63,7 @@ async function downloadAndReplaceLogo(gameDir, progressCallback) { progressCallback('Downloading Logo@2x.png...', null, null, null, null); } - const logoUrl = 'http://3.10.208.30:3002/api/Logo'; + const logoUrl = 'https://files.hytalef2p.com/api/Logo'; const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png'); await downloadFile(logoUrl, tempLogoPath); diff --git a/backend/services/versionManager.js b/backend/services/versionManager.js index 709b74c..cf7b9ba 100644 --- a/backend/services/versionManager.js +++ b/backend/services/versionManager.js @@ -3,7 +3,7 @@ const axios = require('axios'); async function getLatestClientVersion() { try { console.log('Fetching latest client version from API...'); - const response = await axios.get('http://3.10.208.30:3002/api/version_client', { + const response = await axios.get('https://files.hytalef2p.com/api/version_client', { timeout: 5000, headers: { 'User-Agent': 'Hytale-F2P-Launcher' @@ -28,7 +28,7 @@ async function getLatestClientVersion() { async function getInstalledClientVersion() { try { console.log('Fetching installed client version from API...'); - const response = await axios.get('http://3.10.208.30:3002/api/clientCheck', { + const response = await axios.get('https://files.hytalef2p.com/api/clientCheck', { timeout: 5000, headers: { 'User-Agent': 'Hytale-F2P-Launcher' @@ -50,33 +50,7 @@ async function getInstalledClientVersion() { } } -async function getMultiClientVersion() { - try { - console.log('Fetching Multiplayer version from API...'); - const response = await axios.get('http://3.10.208.30:3002/api/multi', { - timeout: 5000, - headers: { - 'User-Agent': 'Hytale-F2P-Launcher' - } - }); - - if (response.data && response.data.multi_version) { - const version = response.data.multi_version; - console.log(`Multiplayer version: ${version}`); - return version; - } else { - console.log('Warning: Invalid multi API response'); - return null; - } - } catch (error) { - console.error('Error fetching Multiplayer version:', error.message); - console.log('Multiplayer not available'); - return null; - } -} - module.exports = { getLatestClientVersion, - getInstalledClientVersion, - getMultiClientVersion + getInstalledClientVersion }; diff --git a/backend/updateManager.js b/backend/updateManager.js index 9346ab7..fea0f0f 100644 --- a/backend/updateManager.js +++ b/backend/updateManager.js @@ -1,7 +1,7 @@ const axios = require('axios'); -const UPDATE_CHECK_URL = 'http://3.10.208.30:3002/api/version_launcher'; -const CURRENT_VERSION = '2.0.1'; +const UPDATE_CHECK_URL = 'https://files.hytalef2p.com/api/version_launcher'; +const CURRENT_VERSION = '2.0.2'; const GITHUB_DOWNLOAD_URL = 'https://github.com/amiayweb/Hytale-F2P/'; class UpdateManager { diff --git a/backend/utils/clientPatcher.js b/backend/utils/clientPatcher.js new file mode 100644 index 0000000..faed895 --- /dev/null +++ b/backend/utils/clientPatcher.js @@ -0,0 +1,468 @@ +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const AdmZip = require('adm-zip'); + +// Domain configuration +const ORIGINAL_DOMAIN = 'hytale.com'; + +function getTargetDomain() { + if (process.env.HYTALE_AUTH_DOMAIN) { + return process.env.HYTALE_AUTH_DOMAIN; + } + try { + const { getAuthDomain } = require('../core/config'); + return getAuthDomain(); + } catch (e) { + return 'sanasol.ws'; + } +} + +const DEFAULT_NEW_DOMAIN = 'sanasol.ws'; + +/** + * Patches HytaleClient and HytaleServer binaries to replace hytale.com with custom domain + * This allows the game to connect to a custom authentication server + */ +class ClientPatcher { + constructor() { + this.patchedFlag = '.patched_custom'; + } + + /** + * Get the target domain for patching + */ + getNewDomain() { + const domain = getTargetDomain(); + if (domain.length !== ORIGINAL_DOMAIN.length) { + console.warn(`Warning: Domain "${domain}" length (${domain.length}) doesn't match original "${ORIGINAL_DOMAIN}" (${ORIGINAL_DOMAIN.length})`); + console.warn(`Using default domain: ${DEFAULT_NEW_DOMAIN}`); + return DEFAULT_NEW_DOMAIN; + } + return domain; + } + + /** + * Convert a string to UTF-16LE bytes (how .NET stores strings) + */ + stringToUtf16LE(str) { + const buf = Buffer.alloc(str.length * 2); + for (let i = 0; i < str.length; i++) { + buf.writeUInt16LE(str.charCodeAt(i), i * 2); + } + return buf; + } + + /** + * Convert a string to UTF-8 bytes (how Java stores strings) + */ + stringToUtf8(str) { + return Buffer.from(str, 'utf8'); + } + + /** + * Find all occurrences of a pattern in a buffer + */ + findAllOccurrences(buffer, pattern) { + const positions = []; + let pos = 0; + while (pos < buffer.length) { + const index = buffer.indexOf(pattern, pos); + if (index === -1) break; + positions.push(index); + pos = index + 1; + } + return positions; + } + + /** + * UTF-8 domain replacement for Java JAR files. + * Java stores strings in UTF-8 format in the constant pool. + */ + findAndReplaceDomainUtf8(data, oldDomain, newDomain) { + let count = 0; + const result = Buffer.from(data); + + const oldUtf8 = this.stringToUtf8(oldDomain); + const newUtf8 = this.stringToUtf8(newDomain); + + const positions = this.findAllOccurrences(result, oldUtf8); + + for (const pos of positions) { + newUtf8.copy(result, pos); + count++; + console.log(` Patched UTF-8 occurrence at offset 0x${pos.toString(16)}`); + } + + return { buffer: result, count }; + } + + /** + * Smart domain replacement that handles both null-terminated and non-null-terminated strings. + * .NET AOT stores some strings in various formats: + * - Standard UTF-16LE (each char is 2 bytes with \x00 high byte) + * - Length-prefixed where last char may have metadata byte instead of \x00 + */ + findAndReplaceDomainSmart(data, oldDomain, newDomain) { + let count = 0; + const result = Buffer.from(data); + + const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1)); + const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1)); + const oldLastChar = this.stringToUtf16LE(oldDomain.slice(-1)); + const newLastChar = this.stringToUtf16LE(newDomain.slice(-1)); + + const oldLastCharByte = oldDomain.charCodeAt(oldDomain.length - 1); + const newLastCharByte = newDomain.charCodeAt(newDomain.length - 1); + + const positions = this.findAllOccurrences(result, oldUtf16NoLast); + + for (const pos of positions) { + const lastCharPos = pos + oldUtf16NoLast.length; + if (lastCharPos + 1 > result.length) continue; + + const lastCharFirstByte = result[lastCharPos]; + + if (lastCharFirstByte === oldLastCharByte) { + newUtf16NoLast.copy(result, pos); + + result[lastCharPos] = newLastCharByte; + + if (lastCharPos + 1 < result.length) { + const secondByte = result[lastCharPos + 1]; + if (secondByte === 0x00) { + console.log(` Patched UTF-16LE occurrence at offset 0x${pos.toString(16)}`); + } else { + console.log(` Patched length-prefixed occurrence at offset 0x${pos.toString(16)} (metadata: 0x${secondByte.toString(16)})`); + } + } + count++; + } + } + + return { buffer: result, count }; + } + + /** + * Check if the client binary has already been patched + */ + isPatchedAlready(clientPath) { + const newDomain = this.getNewDomain(); + const patchFlagFile = clientPath + this.patchedFlag; + if (fs.existsSync(patchFlagFile)) { + try { + const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8')); + if (flagData.targetDomain === newDomain) { + return true; + } + } catch (e) { + } + } + return false; + } + + /** + * Mark the client as patched + */ + markAsPatched(clientPath) { + const newDomain = this.getNewDomain(); + const patchFlagFile = clientPath + this.patchedFlag; + const flagData = { + patchedAt: new Date().toISOString(), + originalDomain: ORIGINAL_DOMAIN, + targetDomain: newDomain, + patcherVersion: '1.0.0' + }; + fs.writeFileSync(patchFlagFile, JSON.stringify(flagData, null, 2)); + } + + /** + * Create a backup of the original client binary + */ + backupClient(clientPath) { + const backupPath = clientPath + '.original'; + if (!fs.existsSync(backupPath)) { + console.log(` Creating backup at ${path.basename(backupPath)}`); + fs.copyFileSync(clientPath, backupPath); + return backupPath; + } + console.log(' Backup already exists'); + return backupPath; + } + + /** + * Restore the original client binary from backup + */ + restoreClient(clientPath) { + const backupPath = clientPath + '.original'; + if (fs.existsSync(backupPath)) { + fs.copyFileSync(backupPath, clientPath); + const patchFlagFile = clientPath + this.patchedFlag; + if (fs.existsSync(patchFlagFile)) { + fs.unlinkSync(patchFlagFile); + } + console.log('Client restored from backup'); + return true; + } + console.log('No backup found to restore'); + return false; + } + + /** + * Patch the client binary to use the custom domain + * @param {string} clientPath - Path to the HytaleClient binary + * @param {function} progressCallback - Optional callback for progress updates + * @returns {object} Result object with success status and details + */ + async patchClient(clientPath, progressCallback) { + const newDomain = this.getNewDomain(); + console.log('=== Client Patcher ==='); + console.log(`Target: ${clientPath}`); + console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`); + + if (!fs.existsSync(clientPath)) { + const error = `Client binary not found: ${clientPath}`; + console.error(error); + return { success: false, error }; + } + + if (this.isPatchedAlready(clientPath)) { + console.log(`Client already patched for ${newDomain}, skipping`); + if (progressCallback) { + progressCallback('Client already patched', 100); + } + return { success: true, alreadyPatched: true, patchCount: 0 }; + } + + if (progressCallback) { + progressCallback('Preparing to patch client...', 10); + } + + console.log('Creating backup...'); + this.backupClient(clientPath); + + if (progressCallback) { + progressCallback('Reading client binary...', 20); + } + + console.log('Reading client binary...'); + const data = fs.readFileSync(clientPath); + console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`); + + if (progressCallback) { + progressCallback('Patching domain references...', 50); + } + + console.log('Patching domain references...'); + const { buffer: patchedData, count } = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, newDomain); + + if (count === 0) { + console.log('No occurrences of hytale.com found - binary may already be modified or has different format'); + return { success: true, patchCount: 0, warning: 'No domain occurrences found' }; + } + + if (progressCallback) { + progressCallback('Writing patched binary...', 80); + } + + console.log('Writing patched binary...'); + fs.writeFileSync(clientPath, patchedData); + + this.markAsPatched(clientPath); + + if (progressCallback) { + progressCallback('Patching complete', 100); + } + + console.log(`Successfully patched ${count} occurrences`); + console.log('=== Patching Complete ==='); + + return { success: true, patchCount: count }; + } + + /** + * Patch the server JAR to use the custom domain + * JAR files are ZIP archives, so we need to extract, patch class files, and repackage + * @param {string} serverPath - Path to the HytaleServer.jar + * @param {function} progressCallback - Optional callback for progress updates + * @returns {object} Result object with success status and details + */ + async patchServer(serverPath, progressCallback) { + const newDomain = this.getNewDomain(); + console.log('=== Server Patcher ==='); + console.log(`Target: ${serverPath}`); + console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`); + + if (!fs.existsSync(serverPath)) { + const error = `Server JAR not found: ${serverPath}`; + console.error(error); + return { success: false, error }; + } + + if (this.isPatchedAlready(serverPath)) { + console.log(`Server already patched for ${newDomain}, skipping`); + if (progressCallback) { + progressCallback('Server already patched', 100); + } + return { success: true, alreadyPatched: true, patchCount: 0 }; + } + + if (progressCallback) { + progressCallback('Preparing to patch server...', 10); + } + + console.log('Creating backup...'); + this.backupClient(serverPath); + + if (progressCallback) { + progressCallback('Extracting server JAR...', 20); + } + + console.log('Opening server JAR...'); + const zip = new AdmZip(serverPath); + const entries = zip.getEntries(); + console.log(`JAR contains ${entries.length} entries`); + + if (progressCallback) { + progressCallback('Patching class files...', 40); + } + + let totalCount = 0; + const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN); + const newUtf8 = this.stringToUtf8(newDomain); + + for (const entry of entries) { + const name = entry.entryName; + if (name.endsWith('.class') || name.endsWith('.properties') || + name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) { + + const data = entry.getData(); + + if (data.includes(oldUtf8)) { + const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, newDomain); + if (count > 0) { + zip.updateFile(entry.entryName, patchedData); + console.log(` Patched ${count} occurrences in ${name}`); + totalCount += count; + } + } + } + } + + if (totalCount === 0) { + console.log('No occurrences of hytale.com found in server JAR entries'); + return { success: true, patchCount: 0, warning: 'No domain occurrences found in JAR' }; + } + + if (progressCallback) { + progressCallback('Writing patched JAR...', 80); + } + + console.log('Writing patched JAR...'); + zip.writeZip(serverPath); + + this.markAsPatched(serverPath); + + if (progressCallback) { + progressCallback('Server patching complete', 100); + } + + console.log(`Successfully patched ${totalCount} occurrences in server`); + console.log('=== Server Patching Complete ==='); + + return { success: true, patchCount: totalCount }; + } + + /** + * Find the client binary path based on platform + */ + findClientPath(gameDir) { + const candidates = []; + + if (process.platform === 'darwin') { + candidates.push(path.join(gameDir, 'Client', 'Hytale.app', 'Contents', 'MacOS', 'HytaleClient')); + candidates.push(path.join(gameDir, 'Client', 'HytaleClient')); + } else if (process.platform === 'win32') { + candidates.push(path.join(gameDir, 'Client', 'HytaleClient.exe')); + } else { + candidates.push(path.join(gameDir, 'Client', 'HytaleClient')); + } + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + return null; + } + + + findServerPath(gameDir) { + const candidates = [ + path.join(gameDir, 'Server', 'HytaleServer.jar'), + path.join(gameDir, 'Server', 'server.jar') + ]; + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + return null; + } + + /** + * Ensure both client and server are patched before launching + * @param {string} gameDir - Path to the game directory + * @param {function} progressCallback - Optional callback for progress updates + */ + async ensureClientPatched(gameDir, progressCallback) { + const results = { + client: null, + server: null, + success: true + }; + + const clientPath = this.findClientPath(gameDir); + if (clientPath) { + if (progressCallback) { + progressCallback('Patching client binary...', 10); + } + results.client = await this.patchClient(clientPath, (msg, pct) => { + if (progressCallback) { + progressCallback(`Client: ${msg}`, pct ? pct / 2 : null); + } + }); + } else { + console.warn('Could not find HytaleClient binary'); + results.client = { success: false, error: 'Client binary not found' }; + } + + const serverPath = this.findServerPath(gameDir); + if (serverPath) { + if (progressCallback) { + progressCallback('Patching server JAR...', 50); + } + results.server = await this.patchServer(serverPath, (msg, pct) => { + if (progressCallback) { + progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null); + } + }); + } else { + console.warn('Could not find HytaleServer.jar'); + results.server = { success: false, error: 'Server JAR not found' }; + } + + results.success = (results.client && results.client.success) || (results.server && results.server.success); + results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched); + results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0); + + if (progressCallback) { + progressCallback('Patching complete', 100); + } + + return results; + } +} + +module.exports = new ClientPatcher(); \ No newline at end of file diff --git a/backend/utils/fileManager.js b/backend/utils/fileManager.js index 492c0c8..0671068 100644 --- a/backend/utils/fileManager.js +++ b/backend/utils/fileManager.js @@ -2,42 +2,150 @@ const fs = require('fs'); const path = require('path'); const axios = require('axios'); -async function downloadFile(url, dest, progressCallback) { - const response = await axios({ - method: 'GET', - url: url, - responseType: 'stream', - headers: { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', - 'Accept': '*/*', - 'Accept-Language': 'en-US,en;q=0.9', - 'Referer': 'https://launcher.hytale.com/' +async function downloadFile(url, dest, progressCallback, maxRetries = 3) { + let lastError = null; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`); + + if (attempt > 0 && progressCallback) { + progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null); + await new Promise(resolve => setTimeout(resolve, 2000 * attempt)); // Délai progressif + } + + const response = await axios({ + method: 'GET', + url: url, + responseType: 'stream', + timeout: 60000, // 60 secondes timeout + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': '*/*', + 'Accept-Language': 'en-US,en;q=0.9', + 'Referer': 'https://launcher.hytale.com/', + 'Connection': 'keep-alive' + }, + // Configuration Axios pour la robustesse réseau + validateStatus: function (status) { + return status >= 200 && status < 300; + }, + // Retry configuration + maxRedirects: 5, + // Network resilience + family: 4 // Force IPv4 + }); + + const totalSize = parseInt(response.headers['content-length'], 10); + let downloaded = 0; + let lastProgressTime = Date.now(); + const startTime = Date.now(); + + // Nettoyer le fichier de destination s'il existe + if (fs.existsSync(dest)) { + fs.unlinkSync(dest); + } + + const writer = fs.createWriteStream(dest); + let downloadStalled = false; + let stalledTimeout = null; + + response.data.on('data', (chunk) => { + downloaded += chunk.length; + const now = Date.now(); + + // Reset stalled timer on data received + if (stalledTimeout) { + clearTimeout(stalledTimeout); + } + + // Set new stalled timer (30 seconds without data = stalled) + stalledTimeout = setTimeout(() => { + downloadStalled = true; + writer.destroy(); + response.data.destroy(); + }, 30000); + + if (progressCallback && totalSize > 0 && (now - lastProgressTime > 100)) { // Update every 100ms max + const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100)); + const elapsed = (now - startTime) / 1000; + const speed = elapsed > 0 ? downloaded / elapsed : 0; + progressCallback(null, percent, speed, downloaded, totalSize); + lastProgressTime = now; + } + }); + + response.data.on('error', (error) => { + if (stalledTimeout) { + clearTimeout(stalledTimeout); + } + console.error(`Stream error on attempt ${attempt + 1}:`, error.code || error.message); + writer.destroy(); + }); + + response.data.pipe(writer); + + await new Promise((resolve, reject) => { + writer.on('finish', () => { + if (stalledTimeout) { + clearTimeout(stalledTimeout); + } + if (!downloadStalled) { + console.log(`Download completed successfully on attempt ${attempt + 1}`); + resolve(); + } else { + reject(new Error('Download stalled')); + } + }); + + writer.on('error', (error) => { + if (stalledTimeout) { + clearTimeout(stalledTimeout); + } + reject(error); + }); + + response.data.on('error', (error) => { + if (stalledTimeout) { + clearTimeout(stalledTimeout); + } + reject(error); + }); + }); + + // Si on arrive ici, le téléchargement a réussi + return; + + } catch (error) { + lastError = error; + console.error(`Download attempt ${attempt + 1} failed:`, error.code || error.message); + + // Nettoyer le fichier partiel en cas d'erreur + if (fs.existsSync(dest)) { + try { + fs.unlinkSync(dest); + } catch (cleanupError) { + console.warn('Could not cleanup partial file:', cleanupError.message); + } + } + + // Vérifier si c'est une erreur réseau que l'on peut retry + const retryableErrors = ['ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'EPROTO']; + const isRetryable = retryableErrors.includes(error.code) || + error.message.includes('timeout') || + error.message.includes('stalled') || + (error.response && error.response.status >= 500); + + if (!isRetryable || attempt === maxRetries - 1) { + console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`); + break; + } + + console.log(`Retryable error detected, will retry in ${2000 * (attempt + 1)}ms...`); } - }); - - const totalSize = parseInt(response.headers['content-length'], 10); - let downloaded = 0; - const startTime = Date.now(); - - const writer = fs.createWriteStream(dest); - - response.data.on('data', (chunk) => { - downloaded += chunk.length; - if (progressCallback && totalSize > 0) { - const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100)); - const elapsed = (Date.now() - startTime) / 1000; - const speed = elapsed > 0 ? downloaded / elapsed : 0; - progressCallback(null, percent, speed, downloaded, totalSize); - } - }); - - response.data.pipe(writer); - - return new Promise((resolve, reject) => { - writer.on('finish', resolve); - writer.on('error', reject); - response.data.on('error', reject); - }); + } + + throw new Error(`Download failed after ${maxRetries} attempts. Last error: ${lastError?.code || lastError?.message || 'Unknown error'}`); } function findHomePageUIPath(gameLatest) { diff --git a/main.js b/main.js index 8874c8f..10719a4 100644 --- a/main.js +++ b/main.js @@ -1,7 +1,7 @@ const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const path = require('path'); const fs = require('fs'); -const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, isGameInstalled, uninstallGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); +const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, isGameInstalled, uninstallGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); const UpdateManager = require('./backend/updateManager'); const logger = require('./backend/logger'); @@ -16,6 +16,13 @@ const DISCORD_CLIENT_ID = '1462244937868513373'; function initDiscordRPC() { try { + // Check if Discord RPC is enabled in settings + const rpcEnabled = loadDiscordRPC(); + if (!rpcEnabled) { + console.log('Discord RPC disabled in settings'); + return; + } + const { Client } = require('discord-rpc'); discordRPC = new Client({ transport: 'ipc' }); @@ -57,6 +64,26 @@ function setDiscordActivity() { } } +function toggleDiscordRPC(enabled) { + console.log('Toggling Discord RPC:', enabled); + + if (enabled && !discordRPC) { + console.log('Initializing Discord RPC...'); + initDiscordRPC(); + } else if (!enabled && discordRPC) { + try { + console.log('Disconnecting Discord RPC...'); + discordRPC.clearActivity(); + discordRPC.destroy(); + discordRPC = null; + console.log('Discord RPC disconnected successfully'); + } catch (error) { + console.error('Error disconnecting Discord RPC:', error.message); + discordRPC = null; // Force null même en cas d'erreur + } + } +} + function createWindow() { mainWindow = new BrowserWindow({ width: 1280, @@ -69,13 +96,19 @@ function createWindow() { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true, - devTools: true, + devTools: false, webSecurity: true } }); mainWindow.loadFile('GUI/index.html'); + // Cleanup Discord RPC when window is closed + mainWindow.on('closed', () => { + console.log('Main window closed, cleaning up Discord RPC...'); + cleanupDiscordRPC(); + }); + // Initialize Discord Rich Presence initDiscordRPC(); @@ -86,6 +119,7 @@ function createWindow() { mainWindow.webContents.send('show-update-popup', updateInfo); } }, 3000); + //mainWindow.webContents.openDevTools(); mainWindow.webContents.on('devtools-opened', () => { @@ -207,17 +241,35 @@ app.whenReady().then(async () => { }, 3000); }); +function cleanupDiscordRPC() { + if (discordRPC) { + try { + console.log('Cleaning up Discord RPC...'); + discordRPC.clearActivity(); + setTimeout(() => { + try { + discordRPC.destroy(); + } catch (error) { + console.log('Error during final Discord RPC cleanup:', error.message); + } + }, 100); + discordRPC = null; + } catch (error) { + console.log('Error cleaning up Discord RPC:', error.message); + discordRPC = null; + } + } +} + +app.on('before-quit', () => { + console.log('=== LAUNCHER BEFORE QUIT ==='); + cleanupDiscordRPC(); +}); + app.on('window-all-closed', () => { console.log('=== LAUNCHER CLOSING ==='); - // Clean up Discord RPC connection - if (discordRPC) { - try { - discordRPC.destroy(); - } catch (error) { - console.log('Error cleaning up Discord RPC:', error.message); - } - } + cleanupDiscordRPC(); if (process.platform !== 'darwin') { app.quit(); @@ -241,16 +293,17 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath) = const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath); + return result; + } catch (error) { + console.error('Launch error:', error); + const errorMessage = error.message || error.toString(); + if (mainWindow && !mainWindow.isDestroyed()) { setTimeout(() => { mainWindow.webContents.send('progress-complete'); }, 2000); } - return result; - } catch (error) { - console.error('Launch error:', error); - const errorMessage = error.message || error.toString(); return { success: false, error: errorMessage }; } }); @@ -272,16 +325,11 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath) const result = await installGame(playerName, progressCallback, javaPath, installPath); - if (mainWindow && !mainWindow.isDestroyed()) { - setTimeout(() => { - mainWindow.webContents.send('progress-complete'); - }, 1000); - } - return result; } catch (error) { console.error('Install error:', error); const errorMessage = error.message || error.toString(); + return { success: false, error: errorMessage }; } }); @@ -301,6 +349,16 @@ ipcMain.handle('save-chat-username', async (event, chatUsername) => { ipcMain.handle('load-chat-username', async () => { return loadChatUsername(); }); + +ipcMain.handle('save-chat-color', (event, color) => { + saveChatColor(color); + return { success: true }; +}); + +ipcMain.handle('load-chat-color', () => { + return loadChatColor(); +}); + ipcMain.handle('save-java-path', (event, javaPath) => { saveJavaPath(javaPath); return { success: true }; @@ -320,6 +378,16 @@ ipcMain.handle('load-install-path', () => { return loadInstallPath(); }); +ipcMain.handle('save-discord-rpc', (event, enabled) => { + saveDiscordRPC(enabled); + toggleDiscordRPC(enabled); + return { success: true }; +}); + +ipcMain.handle('load-discord-rpc', () => { + return loadDiscordRPC(); +}); + ipcMain.handle('select-install-path', async () => { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'], @@ -503,7 +571,7 @@ ipcMain.handle('load-settings', async () => { } }); -const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod } = require('./backend/launcher'); +const { getModsPath, loadInstalledMods, downloadMod, uninstallMod, toggleMod, getCurrentUuid, getAllUuidMappings, setUuidForUser, generateNewUuid, deleteUuidForUser, resetCurrentUserUuid } = require('./backend/launcher'); const os = require('os'); ipcMain.handle('get-local-app-data', async () => { @@ -652,12 +720,73 @@ ipcMain.handle('get-log-directory', () => { return logger.getLogDirectory(); }); +ipcMain.handle('get-current-uuid', async () => { + try { + return getCurrentUuid(); + } catch (error) { + console.error('Error getting current UUID:', error); + return null; + } +}); + +ipcMain.handle('get-all-uuid-mappings', async () => { + try { + const mappings = getAllUuidMappings(); + return Object.entries(mappings).map(([username, uuid]) => ({ + username, + uuid, + isCurrent: username === require('./backend/launcher').loadUsername() + })); + } catch (error) { + console.error('Error getting UUID mappings:', error); + return []; + } +}); + +ipcMain.handle('set-uuid-for-user', async (event, username, uuid) => { + try { + await setUuidForUser(username, uuid); + return { success: true }; + } catch (error) { + console.error('Error setting UUID for user:', error); + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('generate-new-uuid', async () => { + try { + return generateNewUuid(); + } catch (error) { + console.error('Error generating new UUID:', error); + return null; + } +}); + +ipcMain.handle('delete-uuid-for-user', async (event, username) => { + try { + const result = deleteUuidForUser(username); + return { success: result }; + } catch (error) { + console.error('Error deleting UUID for user:', error); + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('reset-current-user-uuid', async () => { + try { + const newUuid = resetCurrentUserUuid(); + return { success: true, uuid: newUuid }; + } catch (error) { + console.error('Error resetting current user UUID:', error); + return { success: false, error: error.message }; + } +}); + ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => { try { const logDir = logger.getLogDirectory(); if (!logDir) return null; - // Find the most recent log file const files = fs.readdirSync(logDir) .filter(file => file.startsWith('launcher-') && file.endsWith('.log')) .map(file => ({ @@ -679,3 +808,4 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => { return null; } }); + diff --git a/package.json b/package.json index b10a6cf..986f9eb 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "hytale-f2p-launcher", + "name": "hytale-f2p-launcherv2", "version": "2.0.1", "description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support", "homepage": "https://github.com/amiayweb/Hytale-F2P", diff --git a/preload.js b/preload.js index 8d0a5ff..466cb9e 100644 --- a/preload.js +++ b/preload.js @@ -9,10 +9,14 @@ contextBridge.exposeInMainWorld('electronAPI', { loadUsername: () => ipcRenderer.invoke('load-username'), saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername), loadChatUsername: () => ipcRenderer.invoke('load-chat-username'), + saveChatColor: (chatColor) => ipcRenderer.invoke('save-chat-color', chatColor), + loadChatColor: () => ipcRenderer.invoke('load-chat-color'), saveJavaPath: (javaPath) => ipcRenderer.invoke('save-java-path', javaPath), loadJavaPath: () => ipcRenderer.invoke('load-java-path'), saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath), loadInstallPath: () => ipcRenderer.invoke('load-install-path'), + saveDiscordRPC: (enabled) => ipcRenderer.invoke('save-discord-rpc', enabled), + loadDiscordRPC: () => ipcRenderer.invoke('load-discord-rpc'), selectInstallPath: () => ipcRenderer.invoke('select-install-path'), browseJavaPath: () => ipcRenderer.invoke('browse-java-path'), isGameInstalled: () => ipcRenderer.invoke('is-game-installed'), @@ -61,5 +65,13 @@ contextBridge.exposeInMainWorld('electronAPI', { }, getLogDirectory: () => ipcRenderer.invoke('get-log-directory'), - getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines) + getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines), + + // UUID Management methods + getCurrentUuid: () => ipcRenderer.invoke('get-current-uuid'), + getAllUuidMappings: () => ipcRenderer.invoke('get-all-uuid-mappings'), + setUuidForUser: (username, uuid) => ipcRenderer.invoke('set-uuid-for-user', username, uuid), + generateNewUuid: () => ipcRenderer.invoke('generate-new-uuid'), + deleteUuidForUser: (username) => ipcRenderer.invoke('delete-uuid-for-user', username), + resetCurrentUserUuid: () => ipcRenderer.invoke('reset-current-user-uuid') });