mirror of
https://github.com/amiayweb/Hytale-F2P.git
synced 2026-02-26 07:41:45 -03:00
update 2.0.2
This commit is contained in:
184
GUI/index.html
184
GUI/index.html
@@ -205,11 +205,17 @@
|
||||
<i class="fas fa-comments mr-2"></i>
|
||||
PLAYERS CHAT
|
||||
</h2>
|
||||
<div class="chat-header-actions">
|
||||
<button id="chatColorBtn" class="chat-color-btn">
|
||||
<i class="fas fa-palette"></i>
|
||||
<span>Color</span>
|
||||
</button>
|
||||
<div class="chat-online-badge">
|
||||
<i class="fas fa-circle"></i>
|
||||
<span id="chatOnlineCount">0</span> online
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-body">
|
||||
<div id="chatMessages" class="chat-messages">
|
||||
@@ -291,6 +297,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">
|
||||
<i class="fab fa-discord"></i>
|
||||
Discord Integration
|
||||
</h3>
|
||||
|
||||
<div class="settings-option">
|
||||
<label class="settings-checkbox">
|
||||
<input type="checkbox" id="discordRPCCheck" checked />
|
||||
<span class="checkmark"></span>
|
||||
<div class="checkbox-content">
|
||||
<div class="checkbox-title">Enable Discord Rich Presence</div>
|
||||
<div class="checkbox-description">Show your launcher activity on Discord</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">
|
||||
<i class="fas fa-gamepad"></i>
|
||||
@@ -326,6 +350,50 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3 class="settings-section-title">
|
||||
<i class="fas fa-fingerprint"></i>
|
||||
Player UUID Management
|
||||
</h3>
|
||||
|
||||
<div class="settings-option">
|
||||
<div class="settings-input-group">
|
||||
<label class="settings-input-label">Current UUID</label>
|
||||
<div class="uuid-display-container">
|
||||
<input
|
||||
type="text"
|
||||
id="currentUuid"
|
||||
class="settings-input uuid-input"
|
||||
readonly
|
||||
placeholder="Loading UUID..."
|
||||
/>
|
||||
<button id="copyUuidBtn" class="uuid-btn copy-btn" title="Copy UUID">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<button id="regenerateUuidBtn" class="uuid-btn regenerate-btn" title="Generate New UUID">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="settings-hint">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Your unique player identifier for this username
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-option">
|
||||
<div class="settings-button-group">
|
||||
<button id="manageUuidsBtn" class="settings-action-btn">
|
||||
<i class="fas fa-list"></i>
|
||||
<div class="btn-content">
|
||||
<div class="btn-title">Manage All UUIDs</div>
|
||||
<div class="btn-description">View and manage all player UUIDs</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -415,16 +483,84 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UUID Management Modal -->
|
||||
<div id="uuidModal" class="uuid-modal" style="display: none;">
|
||||
<div class="uuid-modal-content">
|
||||
<div class="uuid-modal-header">
|
||||
<h2 class="uuid-modal-title">
|
||||
<i class="fas fa-fingerprint mr-2"></i>
|
||||
UUID Management
|
||||
</h2>
|
||||
<button id="uuidModalClose" class="modal-close-btn">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="uuid-modal-body">
|
||||
<div class="uuid-current-section">
|
||||
<h3 class="uuid-section-title">Current User UUID</h3>
|
||||
<div class="uuid-current-display">
|
||||
<input type="text" id="modalCurrentUuid" class="uuid-display-input" readonly />
|
||||
<button id="modalCopyUuidBtn" class="uuid-action-btn copy-btn" title="Copy UUID">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<button id="modalRegenerateUuidBtn" class="uuid-action-btn regenerate-btn" title="Generate New UUID">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="uuid-list-section">
|
||||
<div class="uuid-list-header">
|
||||
<h3 class="uuid-section-title">All Player UUIDs</h3>
|
||||
<button id="generateNewUuidBtn" class="uuid-generate-btn">
|
||||
<i class="fas fa-plus"></i>
|
||||
Generate New UUID
|
||||
</button>
|
||||
</div>
|
||||
<div id="uuidList" class="uuid-list">
|
||||
<div class="uuid-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
Loading UUIDs...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="uuid-custom-section">
|
||||
<h3 class="uuid-section-title">Set Custom UUID</h3>
|
||||
<div class="uuid-custom-form">
|
||||
<input
|
||||
type="text"
|
||||
id="customUuidInput"
|
||||
class="uuid-input"
|
||||
placeholder="Enter custom UUID (format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"
|
||||
maxlength="36"
|
||||
/>
|
||||
<button id="setCustomUuidBtn" class="uuid-set-btn">
|
||||
<i class="fas fa-check"></i>
|
||||
Set UUID
|
||||
</button>
|
||||
</div>
|
||||
<p class="uuid-custom-hint">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Warning: Setting a custom UUID will change your current player identity
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
|
||||
<div class="flex items-center justify-center text-xs text-gray-400">
|
||||
<span>Made by <a href="https://github.com/amiayweb" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@amiayweb</a></span>
|
||||
<span>Made by <a href="https://github.com/amiayweb" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@amiayweb</a> & <a href="https://github.com/Relyz1993" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@Relyz</a></span>
|
||||
<span class="mx-2">|</span>
|
||||
<span>Contributors:
|
||||
<a href="https://github.com/chasem-dev" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@chasem-dev</a>,
|
||||
<a href="https://github.com/crimera" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@crimera</a>,
|
||||
<a href="https://github.com/sanasol" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@sanasol</a>,
|
||||
<a href="https://github.com/Terromur" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@terromur</a>,
|
||||
<a href="https://github.com/ericiskoolbeans" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a>
|
||||
<a href="https://github.com/ericiskoolbeans" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a>,
|
||||
<a href="https://github.com/fazrigading" target="_blank" class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading</a>
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -442,6 +578,50 @@
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal pour sélectionner la couleur du chat -->
|
||||
<div id="chatColorModal" class="chat-color-modal" style="display: none;">
|
||||
<div class="chat-color-modal-content">
|
||||
<div class="chat-color-modal-header">
|
||||
<h3 class="chat-color-modal-title">
|
||||
<i class="fas fa-palette"></i>
|
||||
Customize Username Color
|
||||
</h3>
|
||||
<button class="modal-close-btn" onclick="closeChatColorModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="chat-color-modal-body">
|
||||
<div id="solidColorSection" class="color-section">
|
||||
<h4>Choose a solid color:</h4>
|
||||
<div class="predefined-colors">
|
||||
<div class="color-option" data-color="#3498db" style="background: #3498db;"></div>
|
||||
<div class="color-option" data-color="#e74c3c" style="background: #e74c3c;"></div>
|
||||
<div class="color-option" data-color="#2ecc71" style="background: #2ecc71;"></div>
|
||||
<div class="color-option" data-color="#f39c12" style="background: #f39c12;"></div>
|
||||
<div class="color-option" data-color="#9b59b6" style="background: #9b59b6;"></div>
|
||||
<div class="color-option" data-color="#1abc9c" style="background: #1abc9c;"></div>
|
||||
<div class="color-option" data-color="#e91e63" style="background: #e91e63;"></div>
|
||||
<div class="color-option" data-color="#ff5722" style="background: #ff5722;"></div>
|
||||
</div>
|
||||
<div class="custom-color-input">
|
||||
<label for="customColor">Custom color:</label>
|
||||
<input type="color" id="customColor" value="#3498db">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="color-preview">
|
||||
<h4>Preview:</h4>
|
||||
<div id="colorPreview" class="preview-username">YourUsername</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-color-modal-footer">
|
||||
<button class="btn-secondary" onclick="closeChatColorModal()">Cancel</button>
|
||||
<button class="btn-primary" onclick="applyChatColor()">Apply Color</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/update.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
154
GUI/js/chat.js
154
GUI/js/chat.js
@@ -3,20 +3,34 @@ 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();
|
||||
return;
|
||||
}
|
||||
|
||||
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 = `<span class="user-badge" style="${badgeStyle}">[${badge.badge}]</span> `;
|
||||
}
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<div class="message-header">
|
||||
<span class="message-username">${escapeHtml(username)}</span>
|
||||
<span class="message-user-info">${badgeHTML}<span class="message-username" style="font-weight: bold;" data-username-color="${userColor}">${escapeHtml(username)}</span></span>
|
||||
<span class="message-time">${time}</span>
|
||||
</div>
|
||||
<div class="message-content">${message}</div>
|
||||
`;
|
||||
|
||||
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()
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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,49 +56,182 @@ 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, '');
|
||||
|
||||
if (result.success) {
|
||||
isDownloading = false;
|
||||
|
||||
if (window.LauncherUI) {
|
||||
window.LauncherUI.updateProgress({ message: 'Game started successfully!' });
|
||||
setTimeout(() => {
|
||||
window.LauncherUI.hideProgress();
|
||||
if (window.electronAPI.minimizeWindow) {
|
||||
window.electronAPI.minimizeWindow();
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.error || 'Launch failed');
|
||||
}
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (window.LauncherUI) {
|
||||
window.LauncherUI.updateProgress({ message: 'Game started successfully!' });
|
||||
setTimeout(() => {
|
||||
window.LauncherUI.hideProgress();
|
||||
resetPlayButton();
|
||||
}, 2000);
|
||||
|
||||
if (result.success) {
|
||||
if (window.electronAPI.minimizeWindow) {
|
||||
setTimeout(() => {
|
||||
window.electronAPI.minimizeWindow();
|
||||
}, 500);
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
console.error('Launch failed:', result.error);
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
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 = `
|
||||
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
||||
<div style="display: flex; align-items: center; gap: 12px; color: #ef4444;">
|
||||
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
|
||||
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">${title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 24px; color: #e5e7eb;">
|
||||
<p style="margin: 0; line-height: 1.5; font-size: 1rem;">${message}</p>
|
||||
</div>
|
||||
<div style="padding: 20px 24px; display: flex; gap: 12px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
<button class="custom-confirm-cancel" style="
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
border: 1px solid rgba(156, 163, 175, 0.3);
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
">${cancelText}</button>
|
||||
<button class="custom-confirm-action" style="
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
">${confirmText}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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...' });
|
||||
if (uninstallBtn) uninstallBtn.disabled = true;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 = `
|
||||
<div style="padding: 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
||||
<div style="display: flex; align-items: center; gap: 12px; color: #9333ea;">
|
||||
<i class="fas fa-exclamation-triangle" style="font-size: 24px;"></i>
|
||||
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">${title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 24px; color: #e5e7eb;">
|
||||
<p style="margin: 0; line-height: 1.5; font-size: 1rem;">${message}</p>
|
||||
</div>
|
||||
<div style="padding: 20px 24px; display: flex; gap: 12px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
<button class="custom-confirm-cancel" style="
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
border: 1px solid rgba(156, 163, 175, 0.3);
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
">${cancelText}</button>
|
||||
<button class="custom-confirm-action" style="
|
||||
background: linear-gradient(135deg, #9333ea, #3b82f6);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
">${confirmText}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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,25 +288,74 @@ 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) {
|
||||
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);
|
||||
@@ -153,3 +401,328 @@ window.SettingsAPI = {
|
||||
getCurrentJavaPath,
|
||||
getCurrentPlayerName
|
||||
};
|
||||
|
||||
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 = `
|
||||
<div class="uuid-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
Loading UUIDs...
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (window.electronAPI && window.electronAPI.getAllUuidMappings) {
|
||||
const mappings = await window.electronAPI.getAllUuidMappings();
|
||||
|
||||
if (mappings.length === 0) {
|
||||
uuidList.innerHTML = `
|
||||
<div class="uuid-loading">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
No UUIDs found
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
uuidList.innerHTML = '';
|
||||
|
||||
for (const mapping of mappings) {
|
||||
const item = document.createElement('div');
|
||||
item.className = `uuid-list-item${mapping.isCurrent ? ' current' : ''}`;
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="uuid-item-info">
|
||||
<div class="uuid-item-username">${escapeHtml(mapping.username)}</div>
|
||||
<div class="uuid-item-uuid">${mapping.uuid}</div>
|
||||
</div>
|
||||
<div class="uuid-item-actions">
|
||||
${mapping.isCurrent ? '<div class="uuid-item-current-badge">Current</div>' : ''}
|
||||
<button class="uuid-item-btn copy" onclick="copyUuid('${mapping.uuid}')" title="Copy UUID">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
${!mapping.isCurrent ? `<button class="uuid-item-btn delete" onclick="deleteUuid('${escapeHtml(mapping.username)}')" title="Delete UUID">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
uuidList.appendChild(item);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading UUIDs:', error);
|
||||
if (uuidList) {
|
||||
uuidList.innerHTML = `
|
||||
<div class="uuid-loading">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
Error loading UUIDs
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = `
|
||||
<i class="fas fa-${type === 'success' ? 'check' : type === 'error' ? 'exclamation-triangle' : 'info-circle'}"></i>
|
||||
${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);
|
||||
}
|
||||
783
GUI/style.css
783
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);
|
||||
}
|
||||
|
||||
36
README.md
36
README.md
@@ -1,18 +1,19 @@
|
||||
# 🎮 Hytale F2P Launcher | Multiplayer Support
|
||||
# 🎮 Hytale F2P Launcher | Multiplayer Support [Windows, MacOS, Linux]
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
**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)**
|
||||
|
||||
[](https://github.com/amiayweb/Hytale-F2P/stargazers)
|
||||
[](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** 🛑
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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.
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/amiayweb/Hytale-F2P/issues)
|
||||
[](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
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
473
SERVER.md
473
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:**
|
||||
`<your_path>\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 <player>` | Give operator status |
|
||||
| `deop <player>` | Remove operator status |
|
||||
| `kick <player>` | Kick a player |
|
||||
| `ban <player>` | Ban a player |
|
||||
| `unban <player>` | Unban a player |
|
||||
| `tp <player> <x> <y> <z>` | 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 <ip:port>` | Server address (default: 0.0.0.0:5520) |
|
||||
| `--auth-mode <mode>` | Authentication mode |
|
||||
| `--universe <path>` | Path to world data |
|
||||
| `--mods <path>` | Path to mods folder |
|
||||
| `--backup` | Enable automatic backups |
|
||||
| `--backup-dir <path>` | Backup directory |
|
||||
| `--backup-frequency <mins>` | Backup interval |
|
||||
| `--owner-name <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
|
||||
|
||||
```
|
||||
<game_path>/
|
||||
├── 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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
468
backend/utils/clientPatcher.js
Normal file
468
backend/utils/clientPatcher.js
Normal file
@@ -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();
|
||||
@@ -2,42 +2,150 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
|
||||
async function downloadFile(url, dest, progressCallback) {
|
||||
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/'
|
||||
}
|
||||
'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;
|
||||
if (progressCallback && totalSize > 0) {
|
||||
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 = (Date.now() - startTime) / 1000;
|
||||
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);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', resolve);
|
||||
writer.on('error', reject);
|
||||
response.data.on('error', reject);
|
||||
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...`);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Download failed after ${maxRetries} attempts. Last error: ${lastError?.code || lastError?.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
function findHomePageUIPath(gameLatest) {
|
||||
|
||||
166
main.js
166
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);
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
console.log('=== LAUNCHER CLOSING ===');
|
||||
|
||||
// Clean up Discord RPC connection
|
||||
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 ===');
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
14
preload.js
14
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')
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user