mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 19:51:47 -03:00
Compare commits
4 Commits
v2.0.4-aut
...
v2.0.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fffc730788 | ||
|
|
c475ec7879 | ||
|
|
7efa0d07b0 | ||
|
|
21f8527ed4 |
184
GUI/index.html
184
GUI/index.html
@@ -205,11 +205,17 @@
|
|||||||
<i class="fas fa-comments mr-2"></i>
|
<i class="fas fa-comments mr-2"></i>
|
||||||
PLAYERS CHAT
|
PLAYERS CHAT
|
||||||
</h2>
|
</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">
|
<div class="chat-online-badge">
|
||||||
<i class="fas fa-circle"></i>
|
<i class="fas fa-circle"></i>
|
||||||
<span id="chatOnlineCount">0</span> online
|
<span id="chatOnlineCount">0</span> online
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="chat-body">
|
<div class="chat-body">
|
||||||
<div id="chatMessages" class="chat-messages">
|
<div id="chatMessages" class="chat-messages">
|
||||||
@@ -291,6 +297,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h3 class="settings-section-title">
|
||||||
|
<i class="fab fa-discord"></i>
|
||||||
|
Discord Integration
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="settings-option">
|
||||||
|
<label class="settings-checkbox">
|
||||||
|
<input type="checkbox" id="discordRPCCheck" checked />
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
<div class="checkbox-content">
|
||||||
|
<div class="checkbox-title">Enable Discord Rich Presence</div>
|
||||||
|
<div class="checkbox-description">Show your launcher activity on Discord</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3 class="settings-section-title">
|
<h3 class="settings-section-title">
|
||||||
<i class="fas fa-gamepad"></i>
|
<i class="fas fa-gamepad"></i>
|
||||||
@@ -326,6 +350,50 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,16 +483,84 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<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">
|
<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 class="mx-2">|</span>
|
||||||
<span>Contributors:
|
<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/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/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/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/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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -442,6 +578,50 @@
|
|||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
<script type="module" src="js/update.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
154
GUI/js/chat.js
154
GUI/js/chat.js
@@ -3,20 +3,34 @@ let socket = null;
|
|||||||
let isAuthenticated = false;
|
let isAuthenticated = false;
|
||||||
let messageQueue = [];
|
let messageQueue = [];
|
||||||
let chatUsername = '';
|
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;
|
const MAX_MESSAGE_LENGTH = 500;
|
||||||
|
|
||||||
|
async function getOrCreatePlayerId() {
|
||||||
|
return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
}
|
||||||
|
|
||||||
export async function initChat() {
|
export async function initChat() {
|
||||||
if (window.electronAPI?.loadChatUsername) {
|
if (window.electronAPI?.loadChatUsername) {
|
||||||
chatUsername = await 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() === '') {
|
if (!chatUsername || chatUsername.trim() === '') {
|
||||||
showUsernameModal();
|
showUsernameModal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setupChatUI();
|
setupChatUI();
|
||||||
|
setupColorSelector();
|
||||||
await connectToChat();
|
await connectToChat();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,13 +150,22 @@ async function connectToChat() {
|
|||||||
reconnectionDelay: 1000
|
reconnectionDelay: 1000
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('connect', () => {
|
socket.on('connect', async () => {
|
||||||
console.log('Connected to chat server');
|
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) => {
|
socket.on('authenticated', (data) => {
|
||||||
isAuthenticated = true;
|
isAuthenticated = true;
|
||||||
|
userBadge = data.badge;
|
||||||
addSystemMessage(`Connected as ${data.username}`);
|
addSystemMessage(`Connected as ${data.username}`);
|
||||||
|
|
||||||
while (messageQueue.length > 0) {
|
while (messageQueue.length > 0) {
|
||||||
@@ -155,7 +178,7 @@ async function connectToChat() {
|
|||||||
if (data.type === 'system') {
|
if (data.type === 'system') {
|
||||||
addSystemMessage(data.message);
|
addSystemMessage(data.message);
|
||||||
} else if (data.type === 'user') {
|
} 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();
|
updateCharCounter();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addUserMessage(username, message, timestamp) {
|
function addUserMessage(username, message, timestamp, userColor = '#3498db', badge = null) {
|
||||||
const chatMessages = document.getElementById('chatMessages');
|
const chatMessages = document.getElementById('chatMessages');
|
||||||
if (!chatMessages) return;
|
if (!chatMessages) return;
|
||||||
|
|
||||||
@@ -238,14 +261,35 @@ function addUserMessage(username, message, timestamp) {
|
|||||||
minute: '2-digit'
|
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 = `
|
messageDiv.innerHTML = `
|
||||||
<div class="message-header">
|
<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>
|
<span class="message-time">${time}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-content">${message}</div>
|
<div class="message-content">${message}</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const usernameElement = messageDiv.querySelector('.message-username');
|
||||||
|
if (usernameElement) {
|
||||||
|
applyUserColorStyle(usernameElement, userColor);
|
||||||
|
}
|
||||||
|
|
||||||
chatMessages.appendChild(messageDiv);
|
chatMessages.appendChild(messageDiv);
|
||||||
scrollToBottom();
|
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 = {
|
window.ChatAPI = {
|
||||||
send: sendMessage,
|
send: sendMessage,
|
||||||
disconnect: () => socket?.disconnect()
|
disconnect: () => socket?.disconnect()
|
||||||
|
|||||||
@@ -33,21 +33,12 @@ export function setupInstallation() {
|
|||||||
|
|
||||||
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
|
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
|
||||||
window.electronAPI.onProgressUpdate((data) => {
|
window.electronAPI.onProgressUpdate((data) => {
|
||||||
|
if (!isDownloading) return;
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.showProgress();
|
|
||||||
window.LauncherUI.updateProgress(data);
|
window.LauncherUI.updateProgress(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.electronAPI && window.electronAPI.onProgressComplete) {
|
|
||||||
window.electronAPI.onProgressComplete(() => {
|
|
||||||
if (window.LauncherUI) {
|
|
||||||
window.LauncherUI.hideProgress();
|
|
||||||
}
|
|
||||||
resetInstallButton();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installGame() {
|
export async function installGame() {
|
||||||
@@ -75,6 +66,7 @@ export async function installGame() {
|
|||||||
window.LauncherUI.showLauncherOrInstall(true);
|
window.LauncherUI.showLauncherOrInstall(true);
|
||||||
const playerNameInput = document.getElementById('playerName');
|
const playerNameInput = document.getElementById('playerName');
|
||||||
if (playerNameInput) playerNameInput.value = playerName;
|
if (playerNameInput) playerNameInput.value = playerName;
|
||||||
|
resetInstallButton();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -210,3 +202,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
setupInstallation();
|
setupInstallation();
|
||||||
await checkGameStatusAndShowInterface();
|
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) {
|
if (window.electronAPI && window.electronAPI.onProgressUpdate) {
|
||||||
window.electronAPI.onProgressUpdate((data) => {
|
window.electronAPI.onProgressUpdate((data) => {
|
||||||
|
if (!isDownloading) return;
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.showProgress();
|
|
||||||
window.LauncherUI.updateProgress(data);
|
window.LauncherUI.updateProgress(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.electronAPI && window.electronAPI.onProgressComplete) {
|
|
||||||
window.electronAPI.onProgressComplete(() => {
|
|
||||||
if (window.LauncherUI) {
|
|
||||||
window.LauncherUI.hideProgress();
|
|
||||||
}
|
|
||||||
resetPlayButton();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function launch() {
|
export async function launch() {
|
||||||
@@ -65,49 +56,182 @@ export async function launch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Starting game...' });
|
||||||
|
|
||||||
if (window.electronAPI && window.electronAPI.launchGame) {
|
if (window.electronAPI && window.electronAPI.launchGame) {
|
||||||
const result = await window.electronAPI.launchGame(playerName, javaPath, '');
|
const result = await window.electronAPI.launchGame(playerName, javaPath, '');
|
||||||
|
|
||||||
if (result.success) {
|
isDownloading = false;
|
||||||
|
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: 'Game started successfully!' });
|
|
||||||
setTimeout(() => {
|
|
||||||
window.LauncherUI.hideProgress();
|
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();
|
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) {
|
} catch (error) {
|
||||||
|
isDownloading = false;
|
||||||
|
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
window.LauncherUI.updateProgress({ message: `Failed: ${error.message}` });
|
|
||||||
setTimeout(() => {
|
|
||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
resetPlayButton();
|
|
||||||
}, 3000);
|
|
||||||
}
|
}
|
||||||
|
resetPlayButton();
|
||||||
|
console.error('Launch error:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uninstallGame() {
|
function showCustomConfirm(message, title = 'Confirm Action', onConfirm, onCancel = null, confirmText = 'Confirm', cancelText = 'Cancel') {
|
||||||
if (!confirm('Are you sure you want to uninstall Hytale? All game files will be deleted.')) {
|
const existingModal = document.querySelector('.custom-confirm-modal');
|
||||||
return;
|
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.showProgress();
|
||||||
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Uninstalling game...' });
|
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Uninstalling game...' });
|
||||||
if (uninstallBtn) uninstallBtn.disabled = true;
|
if (uninstallBtn) uninstallBtn.disabled = true;
|
||||||
|
|||||||
@@ -522,7 +522,6 @@ function showNotification(message, type = 'info', duration = 4000) {
|
|||||||
}, duration);
|
}, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom confirmation modal
|
|
||||||
function showConfirmModal(message, onConfirm, onCancel = null) {
|
function showConfirmModal(message, onConfirm, onCancel = null) {
|
||||||
const existingModal = document.querySelector('.mod-confirm-modal');
|
const existingModal = document.querySelector('.mod-confirm-modal');
|
||||||
if (existingModal) {
|
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 updateInterval = null;
|
||||||
let currentUserId = null;
|
let currentUserId = null;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import './players.js';
|
|||||||
import './chat.js';
|
import './chat.js';
|
||||||
import './settings.js';
|
import './settings.js';
|
||||||
|
|
||||||
// Discord notification functions
|
|
||||||
window.closeDiscordNotification = function() {
|
window.closeDiscordNotification = function() {
|
||||||
const notification = document.getElementById('discordNotification');
|
const notification = document.getElementById('discordNotification');
|
||||||
if (notification) {
|
if (notification) {
|
||||||
@@ -18,23 +17,20 @@ window.closeDiscordNotification = function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show notification after a delay
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const notification = document.getElementById('discordNotification');
|
const notification = document.getElementById('discordNotification');
|
||||||
if (notification) {
|
if (notification) {
|
||||||
// Check if user has previously dismissed the notification
|
|
||||||
const dismissed = localStorage.getItem('discordNotificationDismissed');
|
const dismissed = localStorage.getItem('discordNotificationDismissed');
|
||||||
if (!dismissed) {
|
if (!dismissed) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
notification.style.display = 'flex';
|
notification.style.display = 'flex';
|
||||||
}, 3000); // Show after 3 seconds
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
notification.style.display = 'none';
|
notification.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remember when user closes notification
|
|
||||||
const originalClose = window.closeDiscordNotification;
|
const originalClose = window.closeDiscordNotification;
|
||||||
window.closeDiscordNotification = function() {
|
window.closeDiscordNotification = function() {
|
||||||
localStorage.setItem('discordNotificationDismissed', 'true');
|
localStorage.setItem('discordNotificationDismissed', 'true');
|
||||||
|
|||||||
@@ -4,6 +4,143 @@ let customJavaOptions;
|
|||||||
let customJavaPath;
|
let customJavaPath;
|
||||||
let browseJavaBtn;
|
let browseJavaBtn;
|
||||||
let settingsPlayerName;
|
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() {
|
export function initSettings() {
|
||||||
setupSettingsElements();
|
setupSettingsElements();
|
||||||
@@ -16,6 +153,22 @@ function setupSettingsElements() {
|
|||||||
customJavaPath = document.getElementById('customJavaPath');
|
customJavaPath = document.getElementById('customJavaPath');
|
||||||
browseJavaBtn = document.getElementById('browseJavaBtn');
|
browseJavaBtn = document.getElementById('browseJavaBtn');
|
||||||
settingsPlayerName = document.getElementById('settingsPlayerName');
|
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) {
|
if (customJavaCheck) {
|
||||||
customJavaCheck.addEventListener('change', toggleCustomJava);
|
customJavaCheck.addEventListener('change', toggleCustomJava);
|
||||||
@@ -28,6 +181,51 @@ function setupSettingsElements() {
|
|||||||
if (settingsPlayerName) {
|
if (settingsPlayerName) {
|
||||||
settingsPlayerName.addEventListener('change', savePlayerName);
|
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() {
|
function toggleCustomJava() {
|
||||||
@@ -90,25 +288,74 @@ async function loadCustomJavaPath() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function savePlayerName() {
|
async function saveDiscordRPC() {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.saveUsername && settingsPlayerName) {
|
if (window.electronAPI && window.electronAPI.saveDiscordRPC && discordRPCCheck) {
|
||||||
const playerName = settingsPlayerName.value.trim() || 'Player';
|
const enabled = discordRPCCheck.checked;
|
||||||
await window.electronAPI.saveUsername(playerName);
|
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) {
|
} catch (error) {
|
||||||
console.error('Error saving player name:', error);
|
console.error('Error saving player name:', error);
|
||||||
|
showNotification('Failed to save player name', 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadPlayerName() {
|
async function loadPlayerName() {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.loadUsername && settingsPlayerName) {
|
if (!window.electronAPI || !settingsPlayerName) return;
|
||||||
|
|
||||||
const savedName = await window.electronAPI.loadUsername();
|
const savedName = await window.electronAPI.loadUsername();
|
||||||
if (savedName) {
|
if (savedName) {
|
||||||
settingsPlayerName.value = savedName;
|
settingsPlayerName.value = savedName;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading player name:', error);
|
console.error('Error loading player name:', error);
|
||||||
}
|
}
|
||||||
@@ -117,6 +364,8 @@ async function loadPlayerName() {
|
|||||||
async function loadAllSettings() {
|
async function loadAllSettings() {
|
||||||
await loadCustomJavaPath();
|
await loadCustomJavaPath();
|
||||||
await loadPlayerName();
|
await loadPlayerName();
|
||||||
|
await loadCurrentUuid();
|
||||||
|
await loadDiscordRPC();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openGameLocation() {
|
async function openGameLocation() {
|
||||||
@@ -144,7 +393,6 @@ export function getCurrentPlayerName() {
|
|||||||
return 'Player';
|
return 'Player';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make openGameLocation globally available
|
|
||||||
window.openGameLocation = openGameLocation;
|
window.openGameLocation = openGameLocation;
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initSettings);
|
document.addEventListener('DOMContentLoaded', initSettings);
|
||||||
@@ -153,3 +401,328 @@ window.SettingsAPI = {
|
|||||||
getCurrentJavaPath,
|
getCurrentJavaPath,
|
||||||
getCurrentPlayerName
|
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;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.message-username {
|
.message-username {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #9333ea;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
Type=Application
|
Type=Application
|
||||||
Name=Hytale-F2P
|
Name=Hytale-F2P
|
||||||
Comment=A modern, cross-platform launcher for Hytale with automatic updates and multi-client support
|
Comment=A modern, cross-platform launcher for Hytale with automatic updates and multi-client support
|
||||||
Exec=/opt/Hytale-F2P/hytale-f2p-launcher
|
Exec=/opt/Hytale-F2P/hytale-f2p-launcherv2
|
||||||
Categories=Game;
|
Categories=Game;
|
||||||
Icon=Hytale-F2P
|
Icon=Hytale-F2P
|
||||||
Terminal=false
|
Terminal=false
|
||||||
|
|||||||
7
PKGBUILD
7
PKGBUILD
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Terromur <terromuroz@proton.me>
|
# Maintainer: Terromur <terromuroz@proton.me>
|
||||||
pkgname=Hytale-F2P-git
|
pkgname=Hytale-F2P-git
|
||||||
_pkgname=Hytale-F2P
|
_pkgname=Hytale-F2P
|
||||||
pkgver=2.0.0.r47.gebcfdc4
|
pkgver=2.0.2.r90.g21f8527
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="HyLauncher - unofficial Hytale Launcher for free to play gamers"
|
pkgdesc="HyLauncher - unofficial Hytale Launcher for free to play gamers"
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
@@ -9,11 +9,11 @@ url="https://github.com/amiayweb/Hytale-F2P"
|
|||||||
license=('custom')
|
license=('custom')
|
||||||
makedepends=('npm')
|
makedepends=('npm')
|
||||||
source=("git+$url.git" "Hytale-F2P.desktop")
|
source=("git+$url.git" "Hytale-F2P.desktop")
|
||||||
sha256sums=('SKIP' '46488fada4775d9976d7b7b62f8d1f1f8d9a9a9d8f8aa9af4f2e2153019f6a30')
|
sha256sums=('SKIP' '8c78a6931fade2b0501122980dc238e042b9f6f0292b5ca74c391d7b3c1543c0')
|
||||||
|
|
||||||
pkgver() {
|
pkgver() {
|
||||||
cd "$_pkgname"
|
cd "$_pkgname"
|
||||||
printf "2.0.0.r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
printf "2.0.2.r%s.g%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||||
}
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
@@ -28,4 +28,3 @@ package() {
|
|||||||
install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
|
install -Dm644 "$_pkgname.desktop" "$pkgdir/usr/share/applications/$_pkgname.desktop"
|
||||||
install -Dm644 "$_pkgname/icon.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/$_pkgname.png"
|
install -Dm644 "$_pkgname/icon.png" "$pkgdir/usr/share/icons/hicolor/512x512/apps/$_pkgname.png"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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">
|
<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/stargazers)
|
||||||
[](https://github.com/amiayweb/Hytale-F2P/network/members)
|
[](https://github.com/amiayweb/Hytale-F2P/network/members)
|
||||||
|
|
||||||
⭐ **If you find this project useful, please give it a star!** ⭐
|
⭐ **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>
|
</div>
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
- 💾 **Data Preservation** - Intelligent UserData backup and restoration during updates
|
- 💾 **Data Preservation** - Intelligent UserData backup and restoration during updates
|
||||||
- 🌐 **Cross-Platform** - Full support for Windows, Linux (X11/Wayland), and macOS
|
- 🌐 **Cross-Platform** - Full support for Windows, Linux (X11/Wayland), and macOS
|
||||||
- ☕ **Java Management** - Automatic Java runtime detection and installation
|
- ☕ **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**
|
🛡️ **Advanced Features**
|
||||||
- 📁 **Custom Installation** - Choose your own installation directory
|
- 📁 **Custom Installation** - Choose your own installation directory
|
||||||
@@ -63,7 +64,7 @@ See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https
|
|||||||
#### macOS
|
#### macOS
|
||||||
See [BUILD.md](BUILD.md) for detailed build instructions or [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases) section.
|
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)
|
See [SERVER.md](SERVER.md)
|
||||||
|
|
||||||
|
|
||||||
@@ -77,7 +78,15 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
|
|||||||
|
|
||||||
## 📋 Changelog
|
## 📋 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
|
- 📊 **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
|
- 🔧 **Play Button Fix** - Resolved issue where play button could get stuck in "CHECKING..." state
|
||||||
- 💬 **Discord Integration** - Added closable Discord notification for community engagement
|
- 💬 **Discord Integration** - Added closable Discord notification for community engagement
|
||||||
@@ -129,14 +138,16 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
|
|||||||
|
|
||||||
### 🏆 Project Creator
|
### 🏆 Project Creator
|
||||||
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator*
|
- [**@amiayweb**](https://github.com/amiayweb) - *Lead Developer & Project Creator*
|
||||||
|
- [**@Relyz1993**](https://github.com/Relyz1993) - *Server Helper & Second Developer & Project Creator*
|
||||||
|
|
||||||
### 🌟 Contributors
|
### 🌟 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*
|
- [**@chasem-dev**](https://github.com/chasem-dev) - *Issues fixer*
|
||||||
- [**@crimera**](https://github.com/crimera) - *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*
|
- [**@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">
|
<div align="center">
|
||||||
|
|
||||||
[](https://github.com/amiayweb/Hytale-F2P/issues)
|
**Need help?** Join us: https://discord.gg/gME8rUy3MB
|
||||||
[](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)!
|
|
||||||
|
|
||||||
</div>
|
</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:**
|
DOWNLOAD SERVER FILES HERE: https://discord.gg/MEyWUxt77m
|
||||||
```
|
|
||||||
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:
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### PC with 4 GB RAM
|
## Part 1: Playing with Friends (Online Play)
|
||||||
*Best for small servers / testing*
|
|
||||||
|
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
|
```bash
|
||||||
java -Xms512M -Xmx2G -jar HytaleServer.jar --assets ..\Assets.zip
|
./run_server.sh [OPTIONS]
|
||||||
```
|
```
|
||||||
|
|
||||||
- Uses up to **2 GB**
|
| Option | Description |
|
||||||
- Leaves enough memory for Windows
|
|--------|-------------|
|
||||||
|
| `--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
|
## File Structure
|
||||||
*Good for small communities*
|
|
||||||
|
```
|
||||||
|
<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
|
```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**
|
### Manual Backup
|
||||||
- Stable for most setups
|
|
||||||
|
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
|
## Troubleshooting
|
||||||
*Perfect for large or modded servers*
|
|
||||||
|
### "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
|
```bash
|
||||||
java -Xms2G -Xmx8G -jar HytaleServer.jar --assets ..\Assets.zip
|
./run_server.sh --bind 0.0.0.0:5521
|
||||||
```
|
```
|
||||||
|
|
||||||
- Uses up to **8 GB**
|
### "Out of memory"
|
||||||
- Ideal for heavy worlds and plugins
|
|
||||||
|
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
|
For production servers, use Docker:
|
||||||
- `-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
|
|
||||||
|
|
||||||
|
```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,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
|
||||||
|
|
||||||
// Default auth domain - can be overridden by env var or config
|
// Default auth domain - can be overridden by env var or config
|
||||||
const DEFAULT_AUTH_DOMAIN = 'sanasol.ws';
|
const DEFAULT_AUTH_DOMAIN = 'sanasol.ws';
|
||||||
|
|
||||||
@@ -123,6 +124,15 @@ function loadInstallPath() {
|
|||||||
return config.installPath || '';
|
return config.installPath || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveDiscordRPC(enabled) {
|
||||||
|
saveConfig({ discordRPC: !!enabled });
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDiscordRPC() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config.discordRPC !== undefined ? config.discordRPC : true;
|
||||||
|
}
|
||||||
|
|
||||||
function saveModsToConfig(mods) {
|
function saveModsToConfig(mods) {
|
||||||
try {
|
try {
|
||||||
let config = loadConfig();
|
let config = loadConfig();
|
||||||
@@ -172,6 +182,70 @@ function markAsLaunched() {
|
|||||||
saveConfig({ hasLaunchedBefore: true, firstLaunchDate: new Date().toISOString() });
|
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 = {
|
module.exports = {
|
||||||
loadConfig,
|
loadConfig,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
@@ -179,19 +253,29 @@ module.exports = {
|
|||||||
loadUsername,
|
loadUsername,
|
||||||
saveChatUsername,
|
saveChatUsername,
|
||||||
loadChatUsername,
|
loadChatUsername,
|
||||||
|
saveChatColor,
|
||||||
|
loadChatColor,
|
||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
saveJavaPath,
|
saveJavaPath,
|
||||||
loadJavaPath,
|
loadJavaPath,
|
||||||
saveInstallPath,
|
saveInstallPath,
|
||||||
loadInstallPath,
|
loadInstallPath,
|
||||||
|
saveDiscordRPC,
|
||||||
|
loadDiscordRPC,
|
||||||
saveModsToConfig,
|
saveModsToConfig,
|
||||||
loadModsFromConfig,
|
loadModsFromConfig,
|
||||||
isFirstLaunch,
|
isFirstLaunch,
|
||||||
markAsLaunched,
|
markAsLaunched,
|
||||||
CONFIG_FILE,
|
CONFIG_FILE,
|
||||||
// Auth domain config
|
// Auth server exports
|
||||||
DEFAULT_AUTH_DOMAIN,
|
|
||||||
getAuthDomain,
|
|
||||||
getAuthServerUrl,
|
getAuthServerUrl,
|
||||||
saveAuthDomain
|
getAuthDomain,
|
||||||
|
saveAuthDomain,
|
||||||
|
// UUID Management exports
|
||||||
|
getCurrentUuid,
|
||||||
|
getAllUuidMappings,
|
||||||
|
setUuidForUser,
|
||||||
|
generateNewUuid,
|
||||||
|
deleteUuidForUser,
|
||||||
|
resetCurrentUserUuid
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,16 +7,27 @@ const {
|
|||||||
loadUsername,
|
loadUsername,
|
||||||
saveChatUsername,
|
saveChatUsername,
|
||||||
loadChatUsername,
|
loadChatUsername,
|
||||||
|
saveChatColor,
|
||||||
|
loadChatColor,
|
||||||
saveJavaPath,
|
saveJavaPath,
|
||||||
loadJavaPath,
|
loadJavaPath,
|
||||||
saveInstallPath,
|
saveInstallPath,
|
||||||
loadInstallPath,
|
loadInstallPath,
|
||||||
|
saveDiscordRPC,
|
||||||
|
loadDiscordRPC,
|
||||||
saveModsToConfig,
|
saveModsToConfig,
|
||||||
loadModsFromConfig,
|
loadModsFromConfig,
|
||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
isFirstLaunch,
|
isFirstLaunch,
|
||||||
markAsLaunched,
|
markAsLaunched,
|
||||||
CONFIG_FILE
|
CONFIG_FILE,
|
||||||
|
// UUID Management
|
||||||
|
getCurrentUuid,
|
||||||
|
getAllUuidMappings,
|
||||||
|
setUuidForUser,
|
||||||
|
generateNewUuid,
|
||||||
|
deleteUuidForUser,
|
||||||
|
resetCurrentUserUuid
|
||||||
} = require('./core/config');
|
} = require('./core/config');
|
||||||
|
|
||||||
const { getResolvedAppDir, getModsPath } = require('./core/paths');
|
const { getResolvedAppDir, getModsPath } = require('./core/paths');
|
||||||
@@ -37,8 +48,6 @@ const {
|
|||||||
|
|
||||||
const { getJavaDetection } = require('./managers/javaManager');
|
const { getJavaDetection } = require('./managers/javaManager');
|
||||||
|
|
||||||
const { checkAndInstallMultiClient } = require('./managers/multiClientManager');
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
downloadAndReplaceHomePageUI,
|
downloadAndReplaceHomePageUI,
|
||||||
findHomePageUIPath,
|
findHomePageUIPath,
|
||||||
@@ -85,6 +94,8 @@ module.exports = {
|
|||||||
loadUsername,
|
loadUsername,
|
||||||
saveChatUsername,
|
saveChatUsername,
|
||||||
loadChatUsername,
|
loadChatUsername,
|
||||||
|
saveChatColor,
|
||||||
|
loadChatColor,
|
||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
|
|
||||||
// Java configuration functions
|
// Java configuration functions
|
||||||
@@ -96,6 +107,10 @@ module.exports = {
|
|||||||
saveInstallPath,
|
saveInstallPath,
|
||||||
loadInstallPath,
|
loadInstallPath,
|
||||||
|
|
||||||
|
// Discord RPC functions
|
||||||
|
saveDiscordRPC,
|
||||||
|
loadDiscordRPC,
|
||||||
|
|
||||||
// Version functions
|
// Version functions
|
||||||
getInstalledClientVersion,
|
getInstalledClientVersion,
|
||||||
getLatestClientVersion,
|
getLatestClientVersion,
|
||||||
@@ -106,8 +121,13 @@ module.exports = {
|
|||||||
// Player ID functions
|
// Player ID functions
|
||||||
getOrCreatePlayerId,
|
getOrCreatePlayerId,
|
||||||
|
|
||||||
// Multi-client functions
|
// UUID Management functions
|
||||||
checkAndInstallMultiClient,
|
getCurrentUuid,
|
||||||
|
getAllUuidMappings,
|
||||||
|
setUuidForUser,
|
||||||
|
generateNewUuid,
|
||||||
|
deleteUuidForUser,
|
||||||
|
resetCurrentUserUuid,
|
||||||
|
|
||||||
// Mod management functions
|
// Mod management functions
|
||||||
getModsPath,
|
getModsPath,
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ const { getOS, getArch } = require('../utils/platformUtils');
|
|||||||
const { downloadFile } = require('../utils/fileManager');
|
const { downloadFile } = require('../utils/fileManager');
|
||||||
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
|
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
|
||||||
const { installButler } = require('./butlerManager');
|
const { installButler } = require('./butlerManager');
|
||||||
const { checkAndInstallMultiClient } = require('./multiClientManager');
|
|
||||||
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
||||||
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig } = require('../core/config');
|
const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig } = require('../core/config');
|
||||||
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
|
const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager');
|
||||||
@@ -165,9 +164,6 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
|||||||
|
|
||||||
fs.renameSync(tempUpdateDir, gameDir);
|
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);
|
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
|
||||||
console.log('HomePage.ui update result after update:', homeUIResult);
|
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);
|
const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir);
|
||||||
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir);
|
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir);
|
||||||
|
|
||||||
const multiResult = await checkAndInstallMultiClient(customGameDir, progressCallback);
|
|
||||||
console.log('Multiplayer check result:', multiResult);
|
|
||||||
|
|
||||||
const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
|
const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
|
||||||
console.log('HomePage.ui update result after installation:', homeUIResult);
|
console.log('HomePage.ui update result after installation:', homeUIResult);
|
||||||
|
|
||||||
@@ -334,8 +327,7 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
installed: true,
|
installed: true
|
||||||
multiClient: multiResult
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
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');
|
const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui');
|
||||||
|
|
||||||
await downloadFile(homeUIUrl, tempHomePath);
|
await downloadFile(homeUIUrl, tempHomePath);
|
||||||
@@ -63,7 +63,7 @@ async function downloadAndReplaceLogo(gameDir, progressCallback) {
|
|||||||
progressCallback('Downloading Logo@2x.png...', null, null, null, null);
|
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');
|
const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png');
|
||||||
|
|
||||||
await downloadFile(logoUrl, tempLogoPath);
|
await downloadFile(logoUrl, tempLogoPath);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const axios = require('axios');
|
|||||||
async function getLatestClientVersion() {
|
async function getLatestClientVersion() {
|
||||||
try {
|
try {
|
||||||
console.log('Fetching latest client version from API...');
|
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,
|
timeout: 5000,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'Hytale-F2P-Launcher'
|
'User-Agent': 'Hytale-F2P-Launcher'
|
||||||
@@ -28,7 +28,7 @@ async function getLatestClientVersion() {
|
|||||||
async function getInstalledClientVersion() {
|
async function getInstalledClientVersion() {
|
||||||
try {
|
try {
|
||||||
console.log('Fetching installed client version from API...');
|
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,
|
timeout: 5000,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'Hytale-F2P-Launcher'
|
'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 = {
|
module.exports = {
|
||||||
getLatestClientVersion,
|
getLatestClientVersion,
|
||||||
getInstalledClientVersion,
|
getInstalledClientVersion
|
||||||
getMultiClientVersion
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
|
||||||
const UPDATE_CHECK_URL = 'http://3.10.208.30:3002/api/version_launcher';
|
const UPDATE_CHECK_URL = 'https://files.hytalef2p.com/api/version_launcher';
|
||||||
const CURRENT_VERSION = '2.0.1';
|
const CURRENT_VERSION = '2.0.2';
|
||||||
const GITHUB_DOWNLOAD_URL = 'https://github.com/amiayweb/Hytale-F2P/';
|
const GITHUB_DOWNLOAD_URL = 'https://github.com/amiayweb/Hytale-F2P/';
|
||||||
|
|
||||||
class UpdateManager {
|
class UpdateManager {
|
||||||
|
|||||||
@@ -6,23 +6,18 @@ const AdmZip = require('adm-zip');
|
|||||||
// Domain configuration
|
// Domain configuration
|
||||||
const ORIGINAL_DOMAIN = 'hytale.com';
|
const ORIGINAL_DOMAIN = 'hytale.com';
|
||||||
|
|
||||||
// Get target domain from config or environment
|
|
||||||
function getTargetDomain() {
|
function getTargetDomain() {
|
||||||
// Check environment variable first
|
|
||||||
if (process.env.HYTALE_AUTH_DOMAIN) {
|
if (process.env.HYTALE_AUTH_DOMAIN) {
|
||||||
return process.env.HYTALE_AUTH_DOMAIN;
|
return process.env.HYTALE_AUTH_DOMAIN;
|
||||||
}
|
}
|
||||||
// Try to load from config
|
|
||||||
try {
|
try {
|
||||||
const { getAuthDomain } = require('../core/config');
|
const { getAuthDomain } = require('../core/config');
|
||||||
return getAuthDomain();
|
return getAuthDomain();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Config not available, use default
|
|
||||||
return 'sanasol.ws';
|
return 'sanasol.ws';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default domain - must be exactly 10 characters (same as hytale.com)
|
|
||||||
const DEFAULT_NEW_DOMAIN = 'sanasol.ws';
|
const DEFAULT_NEW_DOMAIN = 'sanasol.ws';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,7 +34,6 @@ class ClientPatcher {
|
|||||||
*/
|
*/
|
||||||
getNewDomain() {
|
getNewDomain() {
|
||||||
const domain = getTargetDomain();
|
const domain = getTargetDomain();
|
||||||
// Validate domain length matches original
|
|
||||||
if (domain.length !== ORIGINAL_DOMAIN.length) {
|
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(`Warning: Domain "${domain}" length (${domain.length}) doesn't match original "${ORIGINAL_DOMAIN}" (${ORIGINAL_DOMAIN.length})`);
|
||||||
console.warn(`Using default domain: ${DEFAULT_NEW_DOMAIN}`);
|
console.warn(`Using default domain: ${DEFAULT_NEW_DOMAIN}`);
|
||||||
@@ -92,11 +86,9 @@ class ClientPatcher {
|
|||||||
const oldUtf8 = this.stringToUtf8(oldDomain);
|
const oldUtf8 = this.stringToUtf8(oldDomain);
|
||||||
const newUtf8 = this.stringToUtf8(newDomain);
|
const newUtf8 = this.stringToUtf8(newDomain);
|
||||||
|
|
||||||
// Find all occurrences of the domain
|
|
||||||
const positions = this.findAllOccurrences(result, oldUtf8);
|
const positions = this.findAllOccurrences(result, oldUtf8);
|
||||||
|
|
||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
// Replace the domain
|
|
||||||
newUtf8.copy(result, pos);
|
newUtf8.copy(result, pos);
|
||||||
count++;
|
count++;
|
||||||
console.log(` Patched UTF-8 occurrence at offset 0x${pos.toString(16)}`);
|
console.log(` Patched UTF-8 occurrence at offset 0x${pos.toString(16)}`);
|
||||||
@@ -115,39 +107,29 @@ class ClientPatcher {
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
const result = Buffer.from(data);
|
const result = Buffer.from(data);
|
||||||
|
|
||||||
// Get UTF-16LE bytes without the last character
|
|
||||||
const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1));
|
const oldUtf16NoLast = this.stringToUtf16LE(oldDomain.slice(0, -1));
|
||||||
const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1));
|
const newUtf16NoLast = this.stringToUtf16LE(newDomain.slice(0, -1));
|
||||||
const oldLastChar = this.stringToUtf16LE(oldDomain.slice(-1));
|
const oldLastChar = this.stringToUtf16LE(oldDomain.slice(-1));
|
||||||
const newLastChar = this.stringToUtf16LE(newDomain.slice(-1));
|
const newLastChar = this.stringToUtf16LE(newDomain.slice(-1));
|
||||||
|
|
||||||
// ASCII code of last characters
|
|
||||||
const oldLastCharByte = oldDomain.charCodeAt(oldDomain.length - 1);
|
const oldLastCharByte = oldDomain.charCodeAt(oldDomain.length - 1);
|
||||||
const newLastCharByte = newDomain.charCodeAt(newDomain.length - 1);
|
const newLastCharByte = newDomain.charCodeAt(newDomain.length - 1);
|
||||||
|
|
||||||
// Find all occurrences of the domain without the last character
|
|
||||||
const positions = this.findAllOccurrences(result, oldUtf16NoLast);
|
const positions = this.findAllOccurrences(result, oldUtf16NoLast);
|
||||||
|
|
||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
// Check if we have the last character following
|
|
||||||
const lastCharPos = pos + oldUtf16NoLast.length;
|
const lastCharPos = pos + oldUtf16NoLast.length;
|
||||||
if (lastCharPos + 1 > result.length) continue;
|
if (lastCharPos + 1 > result.length) continue;
|
||||||
|
|
||||||
// Read the byte at last char position
|
|
||||||
const lastCharFirstByte = result[lastCharPos];
|
const lastCharFirstByte = result[lastCharPos];
|
||||||
|
|
||||||
// Check if first byte matches the last character of old domain
|
|
||||||
if (lastCharFirstByte === oldLastCharByte) {
|
if (lastCharFirstByte === oldLastCharByte) {
|
||||||
// Replace all but last character
|
|
||||||
newUtf16NoLast.copy(result, pos);
|
newUtf16NoLast.copy(result, pos);
|
||||||
|
|
||||||
// Replace just the first byte of the last character (preserve metadata byte if any)
|
|
||||||
result[lastCharPos] = newLastCharByte;
|
result[lastCharPos] = newLastCharByte;
|
||||||
|
|
||||||
// If there's a proper null byte (standard UTF-16LE), also check/preserve it
|
|
||||||
if (lastCharPos + 1 < result.length) {
|
if (lastCharPos + 1 < result.length) {
|
||||||
const secondByte = result[lastCharPos + 1];
|
const secondByte = result[lastCharPos + 1];
|
||||||
// Log what type of occurrence this is
|
|
||||||
if (secondByte === 0x00) {
|
if (secondByte === 0x00) {
|
||||||
console.log(` Patched UTF-16LE occurrence at offset 0x${pos.toString(16)}`);
|
console.log(` Patched UTF-16LE occurrence at offset 0x${pos.toString(16)}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -170,12 +152,10 @@ class ClientPatcher {
|
|||||||
if (fs.existsSync(patchFlagFile)) {
|
if (fs.existsSync(patchFlagFile)) {
|
||||||
try {
|
try {
|
||||||
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
||||||
// Check if patched with same target domain
|
|
||||||
if (flagData.targetDomain === newDomain) {
|
if (flagData.targetDomain === newDomain) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Flag file corrupted, will re-patch
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -240,14 +220,12 @@ class ClientPatcher {
|
|||||||
console.log(`Target: ${clientPath}`);
|
console.log(`Target: ${clientPath}`);
|
||||||
console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`);
|
console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`);
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
if (!fs.existsSync(clientPath)) {
|
if (!fs.existsSync(clientPath)) {
|
||||||
const error = `Client binary not found: ${clientPath}`;
|
const error = `Client binary not found: ${clientPath}`;
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return { success: false, error };
|
return { success: false, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if already patched
|
|
||||||
if (this.isPatchedAlready(clientPath)) {
|
if (this.isPatchedAlready(clientPath)) {
|
||||||
console.log(`Client already patched for ${newDomain}, skipping`);
|
console.log(`Client already patched for ${newDomain}, skipping`);
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -260,7 +238,6 @@ class ClientPatcher {
|
|||||||
progressCallback('Preparing to patch client...', 10);
|
progressCallback('Preparing to patch client...', 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create backup
|
|
||||||
console.log('Creating backup...');
|
console.log('Creating backup...');
|
||||||
this.backupClient(clientPath);
|
this.backupClient(clientPath);
|
||||||
|
|
||||||
@@ -268,7 +245,6 @@ class ClientPatcher {
|
|||||||
progressCallback('Reading client binary...', 20);
|
progressCallback('Reading client binary...', 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the binary
|
|
||||||
console.log('Reading client binary...');
|
console.log('Reading client binary...');
|
||||||
const data = fs.readFileSync(clientPath);
|
const data = fs.readFileSync(clientPath);
|
||||||
console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`);
|
console.log(`Binary size: ${(data.length / 1024 / 1024).toFixed(2)} MB`);
|
||||||
@@ -277,7 +253,6 @@ class ClientPatcher {
|
|||||||
progressCallback('Patching domain references...', 50);
|
progressCallback('Patching domain references...', 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the domain replacement
|
|
||||||
console.log('Patching domain references...');
|
console.log('Patching domain references...');
|
||||||
const { buffer: patchedData, count } = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, newDomain);
|
const { buffer: patchedData, count } = this.findAndReplaceDomainSmart(data, ORIGINAL_DOMAIN, newDomain);
|
||||||
|
|
||||||
@@ -290,11 +265,9 @@ class ClientPatcher {
|
|||||||
progressCallback('Writing patched binary...', 80);
|
progressCallback('Writing patched binary...', 80);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the patched binary
|
|
||||||
console.log('Writing patched binary...');
|
console.log('Writing patched binary...');
|
||||||
fs.writeFileSync(clientPath, patchedData);
|
fs.writeFileSync(clientPath, patchedData);
|
||||||
|
|
||||||
// Mark as patched
|
|
||||||
this.markAsPatched(clientPath);
|
this.markAsPatched(clientPath);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -320,14 +293,12 @@ class ClientPatcher {
|
|||||||
console.log(`Target: ${serverPath}`);
|
console.log(`Target: ${serverPath}`);
|
||||||
console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`);
|
console.log(`Replacing: ${ORIGINAL_DOMAIN} -> ${newDomain}`);
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
if (!fs.existsSync(serverPath)) {
|
if (!fs.existsSync(serverPath)) {
|
||||||
const error = `Server JAR not found: ${serverPath}`;
|
const error = `Server JAR not found: ${serverPath}`;
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return { success: false, error };
|
return { success: false, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if already patched
|
|
||||||
if (this.isPatchedAlready(serverPath)) {
|
if (this.isPatchedAlready(serverPath)) {
|
||||||
console.log(`Server already patched for ${newDomain}, skipping`);
|
console.log(`Server already patched for ${newDomain}, skipping`);
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -340,7 +311,6 @@ class ClientPatcher {
|
|||||||
progressCallback('Preparing to patch server...', 10);
|
progressCallback('Preparing to patch server...', 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create backup
|
|
||||||
console.log('Creating backup...');
|
console.log('Creating backup...');
|
||||||
this.backupClient(serverPath);
|
this.backupClient(serverPath);
|
||||||
|
|
||||||
@@ -348,7 +318,6 @@ class ClientPatcher {
|
|||||||
progressCallback('Extracting server JAR...', 20);
|
progressCallback('Extracting server JAR...', 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the JAR file as a ZIP
|
|
||||||
console.log('Opening server JAR...');
|
console.log('Opening server JAR...');
|
||||||
const zip = new AdmZip(serverPath);
|
const zip = new AdmZip(serverPath);
|
||||||
const entries = zip.getEntries();
|
const entries = zip.getEntries();
|
||||||
@@ -358,20 +327,17 @@ class ClientPatcher {
|
|||||||
progressCallback('Patching class files...', 40);
|
progressCallback('Patching class files...', 40);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch each entry that might contain domain strings
|
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN);
|
const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN);
|
||||||
const newUtf8 = this.stringToUtf8(newDomain);
|
const newUtf8 = this.stringToUtf8(newDomain);
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
// Only patch class files and certain resource files
|
|
||||||
const name = entry.entryName;
|
const name = entry.entryName;
|
||||||
if (name.endsWith('.class') || name.endsWith('.properties') ||
|
if (name.endsWith('.class') || name.endsWith('.properties') ||
|
||||||
name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) {
|
name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) {
|
||||||
|
|
||||||
const data = entry.getData();
|
const data = entry.getData();
|
||||||
|
|
||||||
// Check if this entry contains the domain
|
|
||||||
if (data.includes(oldUtf8)) {
|
if (data.includes(oldUtf8)) {
|
||||||
const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, newDomain);
|
const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, newDomain);
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
@@ -392,11 +358,9 @@ class ClientPatcher {
|
|||||||
progressCallback('Writing patched JAR...', 80);
|
progressCallback('Writing patched JAR...', 80);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the patched JAR
|
|
||||||
console.log('Writing patched JAR...');
|
console.log('Writing patched JAR...');
|
||||||
zip.writeZip(serverPath);
|
zip.writeZip(serverPath);
|
||||||
|
|
||||||
// Mark as patched
|
|
||||||
this.markAsPatched(serverPath);
|
this.markAsPatched(serverPath);
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -416,7 +380,6 @@ class ClientPatcher {
|
|||||||
const candidates = [];
|
const candidates = [];
|
||||||
|
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
// macOS: Check both app bundle and direct binary
|
|
||||||
candidates.push(path.join(gameDir, 'Client', 'Hytale.app', 'Contents', 'MacOS', 'HytaleClient'));
|
candidates.push(path.join(gameDir, 'Client', 'Hytale.app', 'Contents', 'MacOS', 'HytaleClient'));
|
||||||
candidates.push(path.join(gameDir, 'Client', 'HytaleClient'));
|
candidates.push(path.join(gameDir, 'Client', 'HytaleClient'));
|
||||||
} else if (process.platform === 'win32') {
|
} else if (process.platform === 'win32') {
|
||||||
@@ -433,9 +396,7 @@ class ClientPatcher {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the server JAR path
|
|
||||||
*/
|
|
||||||
findServerPath(gameDir) {
|
findServerPath(gameDir) {
|
||||||
const candidates = [
|
const candidates = [
|
||||||
path.join(gameDir, 'Server', 'HytaleServer.jar'),
|
path.join(gameDir, 'Server', 'HytaleServer.jar'),
|
||||||
@@ -462,7 +423,6 @@ class ClientPatcher {
|
|||||||
success: true
|
success: true
|
||||||
};
|
};
|
||||||
|
|
||||||
// Patch client
|
|
||||||
const clientPath = this.findClientPath(gameDir);
|
const clientPath = this.findClientPath(gameDir);
|
||||||
if (clientPath) {
|
if (clientPath) {
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -478,7 +438,6 @@ class ClientPatcher {
|
|||||||
results.client = { success: false, error: 'Client binary not found' };
|
results.client = { success: false, error: 'Client binary not found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch server
|
|
||||||
const serverPath = this.findServerPath(gameDir);
|
const serverPath = this.findServerPath(gameDir);
|
||||||
if (serverPath) {
|
if (serverPath) {
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
@@ -494,7 +453,6 @@ class ClientPatcher {
|
|||||||
results.server = { success: false, error: 'Server JAR not found' };
|
results.server = { success: false, error: 'Server JAR not found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate overall success
|
|
||||||
results.success = (results.client && results.client.success) || (results.server && results.server.success);
|
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.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);
|
results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0);
|
||||||
@@ -507,5 +465,4 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
|
||||||
module.exports = new ClientPatcher();
|
module.exports = new ClientPatcher();
|
||||||
@@ -2,42 +2,150 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const axios = require('axios');
|
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({
|
const response = await axios({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: url,
|
url: url,
|
||||||
responseType: 'stream',
|
responseType: 'stream',
|
||||||
|
timeout: 60000, // 60 secondes timeout
|
||||||
headers: {
|
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',
|
'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': '*/*',
|
||||||
'Accept-Language': 'en-US,en;q=0.9',
|
'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);
|
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||||
let downloaded = 0;
|
let downloaded = 0;
|
||||||
|
let lastProgressTime = Date.now();
|
||||||
const startTime = 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);
|
const writer = fs.createWriteStream(dest);
|
||||||
|
let downloadStalled = false;
|
||||||
|
let stalledTimeout = null;
|
||||||
|
|
||||||
response.data.on('data', (chunk) => {
|
response.data.on('data', (chunk) => {
|
||||||
downloaded += chunk.length;
|
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 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;
|
const speed = elapsed > 0 ? downloaded / elapsed : 0;
|
||||||
progressCallback(null, percent, speed, downloaded, totalSize);
|
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);
|
response.data.pipe(writer);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
writer.on('finish', resolve);
|
writer.on('finish', () => {
|
||||||
writer.on('error', reject);
|
if (stalledTimeout) {
|
||||||
response.data.on('error', reject);
|
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) {
|
function findHomePageUIPath(gameLatest) {
|
||||||
|
|||||||
166
main.js
166
main.js
@@ -1,7 +1,7 @@
|
|||||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
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 UpdateManager = require('./backend/updateManager');
|
||||||
const logger = require('./backend/logger');
|
const logger = require('./backend/logger');
|
||||||
|
|
||||||
@@ -16,6 +16,13 @@ const DISCORD_CLIENT_ID = '1462244937868513373';
|
|||||||
|
|
||||||
function initDiscordRPC() {
|
function initDiscordRPC() {
|
||||||
try {
|
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');
|
const { Client } = require('discord-rpc');
|
||||||
discordRPC = new Client({ transport: 'ipc' });
|
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() {
|
function createWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1280,
|
width: 1280,
|
||||||
@@ -69,13 +96,19 @@ function createWindow() {
|
|||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
devTools: true,
|
devTools: false,
|
||||||
webSecurity: true
|
webSecurity: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.loadFile('GUI/index.html');
|
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
|
// Initialize Discord Rich Presence
|
||||||
initDiscordRPC();
|
initDiscordRPC();
|
||||||
|
|
||||||
@@ -86,6 +119,7 @@ function createWindow() {
|
|||||||
mainWindow.webContents.send('show-update-popup', updateInfo);
|
mainWindow.webContents.send('show-update-popup', updateInfo);
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
//mainWindow.webContents.openDevTools();
|
||||||
|
|
||||||
|
|
||||||
mainWindow.webContents.on('devtools-opened', () => {
|
mainWindow.webContents.on('devtools-opened', () => {
|
||||||
@@ -207,17 +241,35 @@ app.whenReady().then(async () => {
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
function cleanupDiscordRPC() {
|
||||||
console.log('=== LAUNCHER CLOSING ===');
|
|
||||||
|
|
||||||
// Clean up Discord RPC connection
|
|
||||||
if (discordRPC) {
|
if (discordRPC) {
|
||||||
|
try {
|
||||||
|
console.log('Cleaning up Discord RPC...');
|
||||||
|
discordRPC.clearActivity();
|
||||||
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
discordRPC.destroy();
|
discordRPC.destroy();
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error during final Discord RPC cleanup:', error.message);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
discordRPC = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error cleaning up Discord RPC:', error.message);
|
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') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit();
|
app.quit();
|
||||||
@@ -241,16 +293,17 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath) =
|
|||||||
|
|
||||||
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath);
|
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Launch error:', error);
|
||||||
|
const errorMessage = error.message || error.toString();
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mainWindow.webContents.send('progress-complete');
|
mainWindow.webContents.send('progress-complete');
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Launch error:', error);
|
|
||||||
const errorMessage = error.message || error.toString();
|
|
||||||
return { success: false, error: errorMessage };
|
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);
|
const result = await installGame(playerName, progressCallback, javaPath, installPath);
|
||||||
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
setTimeout(() => {
|
|
||||||
mainWindow.webContents.send('progress-complete');
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Install error:', error);
|
console.error('Install error:', error);
|
||||||
const errorMessage = error.message || error.toString();
|
const errorMessage = error.message || error.toString();
|
||||||
|
|
||||||
return { success: false, error: errorMessage };
|
return { success: false, error: errorMessage };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -301,6 +349,16 @@ ipcMain.handle('save-chat-username', async (event, chatUsername) => {
|
|||||||
ipcMain.handle('load-chat-username', async () => {
|
ipcMain.handle('load-chat-username', async () => {
|
||||||
return loadChatUsername();
|
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) => {
|
ipcMain.handle('save-java-path', (event, javaPath) => {
|
||||||
saveJavaPath(javaPath);
|
saveJavaPath(javaPath);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -320,6 +378,16 @@ ipcMain.handle('load-install-path', () => {
|
|||||||
return loadInstallPath();
|
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 () => {
|
ipcMain.handle('select-install-path', async () => {
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
properties: ['openDirectory'],
|
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');
|
const os = require('os');
|
||||||
|
|
||||||
ipcMain.handle('get-local-app-data', async () => {
|
ipcMain.handle('get-local-app-data', async () => {
|
||||||
@@ -652,12 +720,73 @@ ipcMain.handle('get-log-directory', () => {
|
|||||||
return logger.getLogDirectory();
|
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) => {
|
ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
|
||||||
try {
|
try {
|
||||||
const logDir = logger.getLogDirectory();
|
const logDir = logger.getLogDirectory();
|
||||||
if (!logDir) return null;
|
if (!logDir) return null;
|
||||||
|
|
||||||
// Find the most recent log file
|
|
||||||
const files = fs.readdirSync(logDir)
|
const files = fs.readdirSync(logDir)
|
||||||
.filter(file => file.startsWith('launcher-') && file.endsWith('.log'))
|
.filter(file => file.startsWith('launcher-') && file.endsWith('.log'))
|
||||||
.map(file => ({
|
.map(file => ({
|
||||||
@@ -679,3 +808,4 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "hytale-f2p-launcher",
|
"name": "hytale-f2p-launcherv2",
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
||||||
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
"homepage": "https://github.com/amiayweb/Hytale-F2P",
|
||||||
@@ -97,8 +97,7 @@
|
|||||||
{
|
{
|
||||||
"target": "dmg",
|
"target": "dmg",
|
||||||
"arch": [
|
"arch": [
|
||||||
"x64",
|
"universal"
|
||||||
"arm64"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
14
preload.js
14
preload.js
@@ -9,10 +9,14 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
loadUsername: () => ipcRenderer.invoke('load-username'),
|
loadUsername: () => ipcRenderer.invoke('load-username'),
|
||||||
saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername),
|
saveChatUsername: (chatUsername) => ipcRenderer.invoke('save-chat-username', chatUsername),
|
||||||
loadChatUsername: () => ipcRenderer.invoke('load-chat-username'),
|
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),
|
saveJavaPath: (javaPath) => ipcRenderer.invoke('save-java-path', javaPath),
|
||||||
loadJavaPath: () => ipcRenderer.invoke('load-java-path'),
|
loadJavaPath: () => ipcRenderer.invoke('load-java-path'),
|
||||||
saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath),
|
saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath),
|
||||||
loadInstallPath: () => ipcRenderer.invoke('load-install-path'),
|
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'),
|
selectInstallPath: () => ipcRenderer.invoke('select-install-path'),
|
||||||
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
|
browseJavaPath: () => ipcRenderer.invoke('browse-java-path'),
|
||||||
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
|
isGameInstalled: () => ipcRenderer.invoke('is-game-installed'),
|
||||||
@@ -61,5 +65,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getLogDirectory: () => ipcRenderer.invoke('get-log-directory'),
|
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