mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-03-02 17:21:52 -03:00
Compare commits
10 Commits
debug-xsha
...
feature/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5e5ddbcc4 | ||
|
|
e610072fa6 | ||
|
|
a40465cac9 | ||
|
|
57056e5b7a | ||
|
|
fcf041be39 | ||
|
|
d53ac915f3 | ||
|
|
7347910fe9 | ||
|
|
0b861904ba | ||
|
|
ee53911a06 | ||
|
|
43a2b6d004 |
212
GUI/index.html
212
GUI/index.html
@@ -24,7 +24,7 @@
|
||||
height="100%25" filter="url(%23noiseFilter)" opacity="0.1" /%3E%3C/svg%3E')] opacity-20"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full h-screen relative z-10">
|
||||
<div class="flex w-full h-screen relative z-10" style="isolation: isolate">
|
||||
<nav class="sidebar">
|
||||
<div class="sidebar-logo">
|
||||
<img src="./icon.png" alt="Hytale Logo" />
|
||||
@@ -44,10 +44,6 @@
|
||||
<i class="fas fa-box"></i>
|
||||
<span class="nav-tooltip" data-i18n="nav.mods">Mods</span>
|
||||
</div>
|
||||
<div class="nav-item" data-page="news">
|
||||
<i class="fas fa-newspaper"></i>
|
||||
<span class="nav-tooltip" data-i18n="nav.news">News</span>
|
||||
</div>
|
||||
<div class="nav-item" data-page="settings">
|
||||
<i class="fas fa-cog"></i>
|
||||
<span class="nav-tooltip" data-i18n="nav.settings">Settings</span>
|
||||
@@ -56,9 +52,12 @@
|
||||
<i class="fas fa-terminal"></i>
|
||||
<span class="nav-tooltip">Logs</span>
|
||||
</div>
|
||||
<div class="nav-item" onclick="openDiscordExternal()">
|
||||
<i class="fas fa-comments"></i>
|
||||
<span class="nav-tooltip">Community Chat</span>
|
||||
<div class="nav-item" id="matchaNavItem">
|
||||
<i class="fas fa-comments" id="matchaNavIcon"></i>
|
||||
<img id="matchaNavAvatar" class="matcha-nav-avatar" style="display:none" />
|
||||
<span class="matcha-nav-status" id="matchaNavStatus" style="display:none"></span>
|
||||
<span class="nav-tooltip">Matcha!</span>
|
||||
<span id="matchaUnreadBadge" class="matcha-unread-badge" style="display:none"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -77,6 +76,7 @@
|
||||
<button class="identity-btn" onclick="toggleIdentityDropdown()">
|
||||
<i class="fas fa-id-badge"></i>
|
||||
<span id="currentIdentityName">Player</span>
|
||||
<i id="passwordShieldIcon" class="fas fa-unlock password-shield unprotected" data-tooltip="Click to protect identity" onclick="event.stopPropagation(); openPasswordModal()"></i>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
<div class="identity-dropdown" id="identityDropdown">
|
||||
@@ -218,36 +218,38 @@
|
||||
<span data-i18n="play.playButton">PLAY HYTALE</span>
|
||||
</button>
|
||||
|
||||
<div style="display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 12px; font-size: 12px;">
|
||||
<span style="color: #93a3b8;">Telegram:</span>
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://t.me/sanhostnet'); return false;" style="color: #93a3b8; text-decoration: none; display: flex; align-items: center; gap: 4px; transition: color 0.2s;" onmouseover="this.style.color='#60a5fa'" onmouseout="this.style.color='#93a3b8'">
|
||||
<i class="fas fa-users"></i> Group
|
||||
<div id="game-info-bar" class="game-info-bar">
|
||||
<span id="game-version-info" class="game-info-item game-info-loading">loading...</span>
|
||||
<span class="game-info-sep"></span>
|
||||
<span id="game-branch-info" class="game-info-item game-info-loading">...</span>
|
||||
<span id="game-last-played" class="game-info-item" style="display: none;"></span>
|
||||
</div>
|
||||
|
||||
<div class="community-links">
|
||||
<a href="#" class="community-link" onclick="openDiscordExternal(); return false;" title="Community Chat">
|
||||
<i class="fas fa-comments" style="color: #22c55e;"></i>
|
||||
<span>Chat</span>
|
||||
</a>
|
||||
<span style="color: #4b5563;">|</span>
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://t.me/hf2p_og'); return false;" style="color: #93a3b8; text-decoration: none; display: flex; align-items: center; gap: 4px; transition: color 0.2s;" onmouseover="this.style.color='#60a5fa'" onmouseout="this.style.color='#93a3b8'">
|
||||
<i class="fab fa-telegram"></i> Channel
|
||||
<a href="#" class="community-link" onclick="window.electronAPI?.openExternal('https://discord.gg/8WVU24XshK'); return false;" title="Discord">
|
||||
<i class="fab fa-discord" style="color: #5865F2;"></i>
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
<span style="color: #4b5563;">|</span>
|
||||
<a href="#" onclick="openDiscordExternal(); return false;" style="color: #93a3b8; text-decoration: none; display: flex; align-items: center; gap: 4px; transition: color 0.2s;" onmouseover="this.style.color='#60a5fa'" onmouseout="this.style.color='#93a3b8'">
|
||||
<i class="fas fa-comments"></i> Community Chat
|
||||
<a href="#" class="community-link" onclick="window.electronAPI?.openExternal('https://t.me/hf2p_og'); return false;" title="Telegram Channel">
|
||||
<i class="fab fa-telegram" style="color: #26A5E4;"></i>
|
||||
<span>Channel</span>
|
||||
</a>
|
||||
<a href="#" class="community-link" onclick="window.electronAPI?.openExternal('https://t.me/sanhostnet'); return false;" title="Telegram Group">
|
||||
<i class="fab fa-telegram" style="color: #26A5E4;"></i>
|
||||
<span>Group</span>
|
||||
</a>
|
||||
<a href="#" class="community-link" onclick="window.electronAPI?.openExternal('https://git.sanhost.net/sanasol/hytale-f2p'); return false;" title="Source Code">
|
||||
<i class="fab fa-git-alt" style="color: #e2e8f0;"></i>
|
||||
<span>Source</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="news-section">
|
||||
<div class="news-header">
|
||||
<h2 class="news-title">
|
||||
<i class="fas fa-star mr-2"></i>
|
||||
<span data-i18n="play.latestNews">LATEST NEWS</span>
|
||||
</h2>
|
||||
<button class="view-all-btn" onclick="navigateToPage('news')">
|
||||
<span data-i18n="play.viewAll">VIEW ALL</span> <i
|
||||
class="fas fa-arrow-right ml-1"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="newsGrid" class="news-grid-horizontal"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="featured-page" class="page">
|
||||
@@ -300,15 +302,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="news-page" class="page">
|
||||
<div class="news-header">
|
||||
<h2 class="news-title">
|
||||
<i class="fas fa-newspaper mr-2"></i>
|
||||
<span data-i18n="news.title">ALL NEWS</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="allNewsGrid" class="news-grid-full"></div>
|
||||
</div>
|
||||
|
||||
<div id="settings-page" class="page">
|
||||
<div class="settings-container">
|
||||
@@ -697,6 +690,29 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Matcha! Panel -->
|
||||
<div id="matchaPanelBackdrop" class="matcha-panel-backdrop"></div>
|
||||
<div id="matchaPanel" class="matcha-panel">
|
||||
<div class="matcha-panel-header">
|
||||
<div id="matchaPanelHeaderContent" class="matcha-panel-header-content">
|
||||
<span class="matcha-header-title">Matcha!</span>
|
||||
</div>
|
||||
<button class="matcha-panel-close" id="matchaPanelCloseBtn">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="matchaPanelBody" class="matcha-panel-body"></div>
|
||||
</div>
|
||||
|
||||
<!-- Matcha User Profile Popup -->
|
||||
<div id="matchaUserProfileOverlay" class="matcha-user-profile-overlay" style="display:none">
|
||||
<div class="matcha-user-profile-card" id="matchaUserProfileCard">
|
||||
<div class="matcha-user-profile-body" id="matchaUserProfileBody">
|
||||
<div class="matcha-loading"><i class="fas fa-spinner fa-spin"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="myModsModal" class="mods-modal">
|
||||
<div class="mods-modal-content">
|
||||
<div class="mods-modal-header">
|
||||
@@ -830,6 +846,41 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="uuid-advanced-section" style="margin-top: 12px;">
|
||||
<button id="passwordSectionToggle" class="uuid-advanced-toggle">
|
||||
<i class="fas fa-chevron-right uuid-advanced-chevron"></i>
|
||||
<span>Password Protection</span>
|
||||
</button>
|
||||
<div id="passwordSectionContent" class="uuid-advanced-content" style="display: none;">
|
||||
<h3 class="uuid-section-title">Protect Your Identity</h3>
|
||||
<p class="uuid-custom-hint" style="margin-bottom: 12px;">
|
||||
<i class="fas fa-shield-alt"></i>
|
||||
<span>Set a password to prevent others from using your UUID</span>
|
||||
</p>
|
||||
<div id="passwordStatusMsg" class="uuid-custom-hint" style="margin-bottom: 8px; color: #93a3b8;"></div>
|
||||
<div id="passwordSetForm">
|
||||
<div class="uuid-custom-form" style="margin-bottom: 8px;">
|
||||
<input type="password" id="currentPasswordInput" class="uuid-input"
|
||||
placeholder="Current password" style="display: none;" />
|
||||
</div>
|
||||
<div class="uuid-custom-form" style="margin-bottom: 8px;">
|
||||
<input type="password" id="newPasswordInput" class="uuid-input"
|
||||
placeholder="New password (min 6 chars)" />
|
||||
</div>
|
||||
<div class="uuid-custom-form">
|
||||
<button id="setPasswordBtn" class="uuid-set-btn" onclick="handleSetPassword()">
|
||||
<i class="fas fa-lock"></i>
|
||||
<span>Set Password</span>
|
||||
</button>
|
||||
<button id="removePasswordBtn" class="uuid-cancel-btn" onclick="handleRemovePassword()" style="display: none;">
|
||||
<i class="fas fa-unlock"></i>
|
||||
<span>Remove Password</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -861,36 +912,95 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="version-display-bottom">
|
||||
<!-- Password Protection Modal -->
|
||||
<div id="passwordModal" class="uuid-modal" style="display: none;">
|
||||
<div class="uuid-modal-content" style="max-width: 420px;">
|
||||
<div class="uuid-modal-header">
|
||||
<h2 class="uuid-modal-title">
|
||||
<i class="fas fa-shield-alt mr-2"></i>
|
||||
<span>Identity Protection</span>
|
||||
</h2>
|
||||
<button class="modal-close-btn" onclick="closePasswordModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="uuid-modal-body" style="padding: 16px 20px;">
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px; padding: 12px; background: rgba(255,255,255,0.03); border-radius: 8px; border: 1px solid rgba(255,255,255,0.06);">
|
||||
<i class="fas fa-user" style="color: #22c55e; font-size: 1.2em;"></i>
|
||||
<div style="flex:1; min-width:0;">
|
||||
<div id="pwModalName" style="font-weight: 600; font-size: 1em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">Player</div>
|
||||
<div id="pwModalUuid" style="font-size: 0.7em; color: #6b7280; font-family: monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"></div>
|
||||
</div>
|
||||
<div id="pwModalStatusBadge" style="flex-shrink:0;"></div>
|
||||
</div>
|
||||
|
||||
<div id="pwModalStatusText" style="margin-bottom: 14px; font-size: 0.85em; color: #93a3b8; text-align: center;"></div>
|
||||
|
||||
<div id="pwModalSetForm">
|
||||
<div style="margin-bottom: 10px;">
|
||||
<input type="password" id="pwModalCurrentPassword" class="uuid-input"
|
||||
placeholder="Current password" style="display: none; width: 100%;" />
|
||||
</div>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<input type="password" id="pwModalNewPassword" class="uuid-input"
|
||||
placeholder="New password (min 6 chars)" style="width: 100%;" />
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<button id="pwModalSetBtn" class="uuid-set-btn" style="flex:1;" onclick="handlePasswordModalSet()">
|
||||
<i class="fas fa-lock"></i>
|
||||
<span>Set Password</span>
|
||||
</button>
|
||||
<button id="pwModalRemoveBtn" class="uuid-cancel-btn" style="display: none;" onclick="handlePasswordModalRemove()">
|
||||
<i class="fas fa-unlock"></i>
|
||||
<span>Remove</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="pwModalUsernameInfo" style="margin-top: 14px; padding: 10px; background: rgba(34,197,94,0.06); border-radius: 6px; border: 1px solid rgba(34,197,94,0.15); font-size: 0.8em; color: #93a3b8; display: none;">
|
||||
<i class="fas fa-info-circle" style="color: #22c55e;"></i>
|
||||
<span>Setting a password also reserves your username — no one else can use it.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="version-display-bottom" onclick="window.electronAPI?.openExternal('https://git.sanhost.net/sanasol/hytale-f2p/releases')" style="cursor: pointer;" title="View releases">
|
||||
<i class="fas fa-code-branch"></i>
|
||||
<span id="launcherVersion"></span>
|
||||
</div>
|
||||
|
||||
<footer class="fixed bottom-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-sm px-4 py-2">
|
||||
<div class="flex items-center justify-center text-xs text-gray-400">
|
||||
<span>Made by <a href="https://github.com/amiayweb" target="_blank"
|
||||
<span>Made by <a href="#" onclick="window.electronAPI?.openExternal('https://github.com/amiayweb'); return false;"
|
||||
class="text-blue-400 hover:text-blue-300 transition-colors">@amiayweb</a> & <a
|
||||
href="https://github.com/Relyz1993" target="_blank"
|
||||
href="#" onclick="window.electronAPI?.openExternal('https://github.com/Relyz1993'); return false;"
|
||||
class="text-blue-400 hover:text-blue-300 transition-colors">@Relyz</a></span>
|
||||
<span class="mx-2">|</span>
|
||||
<span>Contributors:
|
||||
<a href="https://github.com/chasem-dev" target="_blank"
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://github.com/chasem-dev'); return false;"
|
||||
class="text-blue-400 hover:text-blue-300 transition-colors">@chasem-dev</a>,
|
||||
<a href="https://github.com/crimera" target="_blank"
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://github.com/crimera'); return false;"
|
||||
class="text-blue-400 hover:text-blue-300 transition-colors">@crimera</a>,
|
||||
<a href="https://github.com/sanasol" target="_blank"
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://github.com/sanasol'); return false;"
|
||||
class="text-blue-400 hover:text-blue-300 transition-colors">@sanasol</a>,
|
||||
<a href="https://github.com/Terromur" target="_blank"
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://github.com/Terromur'); return false;"
|
||||
class="text-blue-400 hover:text-blue-300 transition-colors">@terromur</a>,
|
||||
<a href="https://github.com/ericiskoolbeans" target="_blank"
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://github.com/ericiskoolbeans'); return false;"
|
||||
class="text-blue-400 hover:text-blue-300 transition-colors">@ericiskoolbeans</a>,
|
||||
<a href="https://github.com/fazrigading" target="_blank"
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://github.com/fazrigading'); return false;"
|
||||
class="text-blue-400 hover:text-blue-300 transition-colors">@fazrigading</a>,
|
||||
<a href="https://github.com/Rahul-Sahani04" target="_blank"
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://github.com/Rahul-Sahani04'); return false;"
|
||||
class="text-blue-400 hover:text-blue-300 transition-colors">@Rahul-Sahani04</a>,
|
||||
<a href="https://github.com/xSamiVS" target="_blank"
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://github.com/xSamiVS'); return false;"
|
||||
class="text-blue-400 hover:text-blue-300 transition-colors">@xSamiVS</a>
|
||||
</span>
|
||||
<span class="mx-2">|</span>
|
||||
<span>Social powered by <a href="#" onclick="window.electronAPI?.openExternal('https://github.com/vZylev/Butter-Launcher'); return false;"
|
||||
class="text-purple-400 hover:text-purple-300 transition-colors">Butter Launcher</a> &
|
||||
<a href="#" onclick="window.electronAPI?.openExternal('https://butterlauncher.tech/'); return false;"
|
||||
class="text-purple-400 hover:text-purple-300 transition-colors">Matcha!</a>
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -169,6 +169,16 @@ window.switchProfile = async (id) => {
|
||||
// Refresh UI
|
||||
await loadProfiles();
|
||||
|
||||
// Refresh branch radio buttons in settings
|
||||
if (window.SettingsAPI?.reloadBranch) {
|
||||
await window.SettingsAPI.reloadBranch();
|
||||
}
|
||||
|
||||
// Refresh game info bar on play page
|
||||
if (window.LauncherUI?.loadGameInfoBar) {
|
||||
window.LauncherUI.loadGameInfoBar();
|
||||
}
|
||||
|
||||
// Refresh Mods
|
||||
if (window.modsManager) {
|
||||
if (window.modsManager.loadInstalledMods) await window.modsManager.loadInstalledMods();
|
||||
@@ -193,7 +203,8 @@ window.switchProfile = async (id) => {
|
||||
};
|
||||
|
||||
export async function launch() {
|
||||
if (isDownloading || (playBtn && playBtn.disabled)) return;
|
||||
const btn = homePlayBtn || playBtn;
|
||||
if (isDownloading || (btn && btn.disabled)) return;
|
||||
|
||||
// ==========================================================================
|
||||
// STEP 1: Check launch readiness from backend (single source of truth)
|
||||
@@ -271,11 +282,7 @@ export async function launch() {
|
||||
// STEP 3: Start launch process
|
||||
// ==========================================================================
|
||||
if (window.LauncherUI) window.LauncherUI.showProgress();
|
||||
isDownloading = true;
|
||||
if (playBtn) {
|
||||
playBtn.disabled = true;
|
||||
playText.textContent = 'LAUNCHING...';
|
||||
}
|
||||
lockPlayButton('LAUNCHING...');
|
||||
|
||||
try {
|
||||
const startingMsg = window.i18n ? window.i18n.t('progress.startingGame') : 'Starting game...';
|
||||
@@ -285,20 +292,92 @@ export async function launch() {
|
||||
// Pass playerName from config - backend will validate again
|
||||
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
|
||||
|
||||
if (result.usernameTaken) {
|
||||
// Username reserved by another player
|
||||
isDownloading = false;
|
||||
|
||||
if (window.LauncherUI) {
|
||||
window.LauncherUI.hideProgress();
|
||||
}
|
||||
if (window.LauncherUI) window.LauncherUI.hideProgress();
|
||||
resetPlayButton();
|
||||
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||
window.LauncherUI.showError('This username is reserved by another player. Please change your player name in Identity settings.');
|
||||
} else {
|
||||
showNotification('This username is reserved by another player. Please change your player name.', 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.nameLocked) {
|
||||
// UUID is password-protected and locked to a specific name
|
||||
isDownloading = false;
|
||||
if (window.LauncherUI) window.LauncherUI.hideProgress();
|
||||
resetPlayButton();
|
||||
const msg = result.registeredName
|
||||
? `This UUID is locked to username "${result.registeredName}". Change your identity name to "${result.registeredName}" in Settings.`
|
||||
: 'This UUID is locked to a different username. Check your identity settings.';
|
||||
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||
window.LauncherUI.showError(msg);
|
||||
} else {
|
||||
showNotification(msg, 'error');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.passwordRequired) {
|
||||
// Check for saved password first
|
||||
let savedPw = null;
|
||||
try {
|
||||
const cfg = await window.electronAPI.loadConfig();
|
||||
const uuid = result.uuid || '';
|
||||
savedPw = cfg && cfg.savedPasswords && cfg.savedPasswords[uuid] ? cfg.savedPasswords[uuid] : null;
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
if (savedPw) {
|
||||
// Try saved password silently
|
||||
if (window.LauncherUI) window.LauncherUI.showProgress();
|
||||
lockPlayButton('LAUNCHING...');
|
||||
const autoResult = await window.electronAPI.launchGameWithPassword(playerName, javaPath, '', gpuPreference, savedPw);
|
||||
if (autoResult.success) {
|
||||
isDownloading = false;
|
||||
if (window.LauncherUI) window.LauncherUI.hideProgress();
|
||||
resetPlayButton();
|
||||
if (window.electronAPI.minimizeWindow) setTimeout(() => { window.electronAPI.minimizeWindow(); }, 500);
|
||||
return;
|
||||
}
|
||||
// Saved password failed — clear it and show popup
|
||||
isDownloading = false;
|
||||
if (window.LauncherUI) window.LauncherUI.hideProgress();
|
||||
resetPlayButton();
|
||||
try {
|
||||
const cfg2 = await window.electronAPI.loadConfig();
|
||||
const sp = cfg2.savedPasswords || {};
|
||||
delete sp[result.uuid || ''];
|
||||
await window.electronAPI.saveConfig({ savedPasswords: sp });
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
// Show interactive password dialog
|
||||
const launchResult = await promptForPasswordAndLaunch(playerName, javaPath, gpuPreference, result.uuid);
|
||||
if (launchResult && launchResult.success) {
|
||||
if (window.electronAPI.minimizeWindow) setTimeout(() => { window.electronAPI.minimizeWindow(); }, 500);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
// Keep button locked so user can't double-launch
|
||||
if (window.LauncherUI) window.LauncherUI.hideProgress();
|
||||
lockPlayButton('GAME RUNNING');
|
||||
setTimeout(() => {
|
||||
resetPlayButton();
|
||||
}, 10000); // Reset after 10s (game should be visible by then)
|
||||
if (window.electronAPI.minimizeWindow) {
|
||||
setTimeout(() => {
|
||||
window.electronAPI.minimizeWindow();
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
isDownloading = false;
|
||||
if (window.LauncherUI) window.LauncherUI.hideProgress();
|
||||
resetPlayButton();
|
||||
console.error('[Launcher] Launch failed:', result.error);
|
||||
|
||||
// Handle specific error cases
|
||||
@@ -354,6 +433,187 @@ export async function launch() {
|
||||
}
|
||||
}
|
||||
|
||||
function promptForPasswordAndLaunch(playerName, javaPath, gpuPreference, uuid) {
|
||||
return new Promise((resolve) => {
|
||||
// Remove any existing password prompt
|
||||
const existing = document.querySelector('.custom-confirm-modal');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'custom-confirm-modal';
|
||||
overlay.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;
|
||||
`;
|
||||
|
||||
const dialog = document.createElement('div');
|
||||
dialog.style.cssText = `
|
||||
background: #1f2937;
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
min-width: 380px;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
`;
|
||||
|
||||
dialog.innerHTML = `
|
||||
<div style="padding: 20px 24px; border-bottom: 1px solid rgba(255,255,255,0.1);">
|
||||
<div style="display: flex; align-items: center; gap: 10px; color: #f59e0b;">
|
||||
<i class="fas fa-lock" style="font-size: 20px;"></i>
|
||||
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 600; color: #e5e7eb;">Password Required</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 20px 24px;">
|
||||
<p style="margin: 0 0 12px 0; color: #9ca3af; font-size: 0.9rem; line-height: 1.5;">This identity is password-protected. Enter your password to continue.</p>
|
||||
<div id="pwErrorMsg" style="display: none; margin-bottom: 12px; padding: 8px 12px; background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); border-radius: 6px; color: #f87171; font-size: 0.85rem;"></div>
|
||||
<input type="password" id="launchPasswordInput" style="
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 10px 14px;
|
||||
background: rgba(0,0,0,0.3);
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
border-radius: 8px;
|
||||
color: #e5e7eb;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
" placeholder="Password" autofocus />
|
||||
<label style="display: flex; align-items: center; gap: 8px; margin-top: 12px; cursor: pointer; color: #9ca3af; font-size: 0.85rem; user-select: none;">
|
||||
<input type="checkbox" id="pwRememberCheck" style="accent-color: #f59e0b; width: 16px; height: 16px; cursor: pointer;" />
|
||||
Remember password
|
||||
</label>
|
||||
</div>
|
||||
<div style="padding: 16px 24px; display: flex; gap: 10px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
<button id="pwCancelBtn" style="
|
||||
background: transparent;
|
||||
color: #9ca3af;
|
||||
border: 1px solid rgba(156,163,175,0.3);
|
||||
padding: 9px 18px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
">Cancel</button>
|
||||
<button id="pwConfirmBtn" style="
|
||||
background: #f59e0b;
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 9px 18px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
">Login</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.appendChild(dialog);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const input = overlay.querySelector('#launchPasswordInput');
|
||||
const confirmBtn = overlay.querySelector('#pwConfirmBtn');
|
||||
const cancelBtn = overlay.querySelector('#pwCancelBtn');
|
||||
const errorMsg = overlay.querySelector('#pwErrorMsg');
|
||||
const rememberCheck = overlay.querySelector('#pwRememberCheck');
|
||||
|
||||
let busy = false;
|
||||
|
||||
const close = (result) => {
|
||||
overlay.remove();
|
||||
isDownloading = false;
|
||||
if (window.LauncherUI) window.LauncherUI.hideProgress();
|
||||
resetPlayButton();
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
const showError = (msg) => {
|
||||
errorMsg.textContent = msg;
|
||||
errorMsg.style.display = 'block';
|
||||
input.style.borderColor = 'rgba(239,68,68,0.5)';
|
||||
input.value = '';
|
||||
input.focus();
|
||||
};
|
||||
|
||||
const tryLogin = async () => {
|
||||
const password = input.value;
|
||||
if (!password) {
|
||||
showError('Please enter your password.');
|
||||
return;
|
||||
}
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
|
||||
// Show loading state
|
||||
confirmBtn.disabled = true;
|
||||
confirmBtn.textContent = 'Logging in...';
|
||||
errorMsg.style.display = 'none';
|
||||
input.style.borderColor = 'rgba(255,255,255,0.15)';
|
||||
|
||||
try {
|
||||
if (window.LauncherUI) window.LauncherUI.showProgress();
|
||||
lockPlayButton('LAUNCHING...');
|
||||
|
||||
const result = await window.electronAPI.launchGameWithPassword(playerName, javaPath, '', gpuPreference, password);
|
||||
|
||||
if (result.success) {
|
||||
// Save password if "Remember" checked
|
||||
if (rememberCheck.checked && uuid) {
|
||||
try {
|
||||
const cfg = await window.electronAPI.loadConfig();
|
||||
const sp = cfg.savedPasswords || {};
|
||||
sp[uuid] = password;
|
||||
await window.electronAPI.saveConfig({ savedPasswords: sp });
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
overlay.remove();
|
||||
isDownloading = false;
|
||||
if (window.LauncherUI) window.LauncherUI.hideProgress();
|
||||
resetPlayButton();
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wrong password
|
||||
if (result.passwordRequired) {
|
||||
showError(result.error || 'Incorrect password. Please try again.');
|
||||
} else {
|
||||
showError(result.error || 'Launch failed.');
|
||||
}
|
||||
|
||||
isDownloading = false;
|
||||
if (window.LauncherUI) window.LauncherUI.hideProgress();
|
||||
resetPlayButton();
|
||||
} catch (err) {
|
||||
showError(err.message || 'An error occurred.');
|
||||
isDownloading = false;
|
||||
if (window.LauncherUI) window.LauncherUI.hideProgress();
|
||||
resetPlayButton();
|
||||
} finally {
|
||||
busy = false;
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.textContent = 'Login';
|
||||
}
|
||||
};
|
||||
|
||||
confirmBtn.addEventListener('click', tryLogin);
|
||||
cancelBtn.addEventListener('click', () => close(null));
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') tryLogin();
|
||||
if (e.key === 'Escape') close(null);
|
||||
});
|
||||
|
||||
setTimeout(() => input.focus(), 100);
|
||||
});
|
||||
}
|
||||
|
||||
function showCustomConfirm(message, title, onConfirm, onCancel = null, confirmText, cancelText) {
|
||||
// Apply defaults with i18n support
|
||||
title = title || (window.i18n ? window.i18n.t('confirm.defaultTitle') : 'Confirm Action');
|
||||
@@ -589,9 +849,21 @@ async function performRepair() {
|
||||
|
||||
function resetPlayButton() {
|
||||
isDownloading = false;
|
||||
if (playBtn) {
|
||||
playBtn.disabled = false;
|
||||
playText.textContent = window.i18n ? window.i18n.t('play.play') : 'PLAY';
|
||||
const btn = homePlayBtn || playBtn;
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
const textEl = btn.querySelector('span');
|
||||
if (textEl) textEl.textContent = window.i18n ? window.i18n.t('play.playButton') : 'PLAY HYTALE';
|
||||
}
|
||||
}
|
||||
|
||||
function lockPlayButton(text) {
|
||||
isDownloading = true;
|
||||
const btn = homePlayBtn || playBtn;
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
const textEl = btn.querySelector('span');
|
||||
if (textEl) textEl.textContent = text || 'LAUNCHING...';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,7 +984,7 @@ async function loadIdentities() {
|
||||
}
|
||||
}
|
||||
|
||||
function renderIdentityList(mappings, currentUsername) {
|
||||
async function renderIdentityList(mappings, currentUsername) {
|
||||
const list = document.getElementById('identityList');
|
||||
if (!list) return;
|
||||
|
||||
@@ -721,13 +993,31 @@ function renderIdentityList(mappings, currentUsername) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = mappings.map(m => {
|
||||
// Check password status for all identities in parallel
|
||||
const statusChecks = mappings.map(async m => {
|
||||
try {
|
||||
if (m.uuid && window.electronAPI?.checkPasswordStatus) {
|
||||
const s = await window.electronAPI.checkPasswordStatus(m.uuid);
|
||||
return s?.hasPassword || false;
|
||||
}
|
||||
} catch {}
|
||||
return false;
|
||||
});
|
||||
const statuses = await Promise.all(statusChecks);
|
||||
|
||||
list.innerHTML = mappings.map((m, i) => {
|
||||
const safe = escapeHtml(m.username);
|
||||
const isActive = m.username === currentUsername;
|
||||
const hasPassword = statuses[i];
|
||||
const pwBadge = hasPassword
|
||||
? '<span class="pw-badge locked"><i class="fas fa-lock"></i></span>'
|
||||
: '<span class="pw-badge unlocked"><i class="fas fa-unlock"></i></span>';
|
||||
return `
|
||||
<div class="identity-item ${m.username === currentUsername ? 'active' : ''}"
|
||||
<div class="identity-item ${isActive ? 'active' : ''}"
|
||||
onclick="switchIdentity('${safe.replace(/'/g, "'")}')">
|
||||
<span>${safe}</span>
|
||||
${m.username === currentUsername ? '<i class="fas fa-check ml-auto"></i>' : ''}
|
||||
${pwBadge}
|
||||
${isActive ? '<i class="fas fa-check" style="margin-left:4px;"></i>' : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
@@ -774,6 +1064,9 @@ window.switchIdentity = async (username) => {
|
||||
if (settingsInput) settingsInput.value = username;
|
||||
if (window.loadCurrentUuid) window.loadCurrentUuid();
|
||||
|
||||
// Update password shield icon for new identity
|
||||
if (window.updatePasswordShieldIcon) window.updatePasswordShieldIcon();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to switch identity:', error);
|
||||
}
|
||||
|
||||
1920
GUI/js/matcha.js
Normal file
1920
GUI/js/matcha.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
||||
import './ui.js';
|
||||
import './install.js';
|
||||
import './launcher.js';
|
||||
import './news.js';
|
||||
import { initModsManager } from './mods.js';
|
||||
import './players.js';
|
||||
import './settings.js';
|
||||
import './logs.js';
|
||||
import { initMatcha } from './matcha.js';
|
||||
|
||||
let i18nInitialized = false;
|
||||
(async () => {
|
||||
@@ -16,10 +16,12 @@ let i18nInitialized = false;
|
||||
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||
updateLanguageSelector();
|
||||
initModsManager();
|
||||
initMatcha();
|
||||
} else {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateLanguageSelector();
|
||||
initModsManager();
|
||||
initMatcha();
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -515,8 +515,9 @@ async function savePlayerName() {
|
||||
// Also refresh the UUID list to update which entry is marked as current
|
||||
await loadAllUuids();
|
||||
|
||||
// Refresh header identity dropdown
|
||||
// Refresh header identity dropdown + shield icon
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
updatePasswordShieldIcon();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error saving player name:', error);
|
||||
@@ -746,6 +747,7 @@ async function performRegenerateUuid() {
|
||||
await loadAllUuids();
|
||||
}
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
updatePasswordShieldIcon();
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to generate new UUID');
|
||||
}
|
||||
@@ -912,18 +914,61 @@ async function confirmAddIdentity() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.electronAPI && window.electronAPI.setUuidForUser) {
|
||||
const result = await window.electronAPI.setUuidForUser(username, uuid);
|
||||
if (result.success) {
|
||||
const msg = window.i18n ? window.i18n.t('notifications.identityAdded') : 'Identity added successfully!';
|
||||
showNotification(msg, 'success');
|
||||
hideAddIdentityForm();
|
||||
await loadAllUuids();
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to add identity');
|
||||
// Check if name already exists locally
|
||||
if (window.electronAPI && window.electronAPI.getAllUuidMappings) {
|
||||
const mappings = await window.electronAPI.getAllUuidMappings();
|
||||
const existing = mappings.find(m => m.username.toLowerCase() === username.toLowerCase());
|
||||
if (existing) {
|
||||
showNotification(`Identity "${existing.username}" already exists (UUID: ${existing.uuid.substring(0, 8)}...). Use the identity list to manage it.`, 'error');
|
||||
return;
|
||||
}
|
||||
// Check if UUID already used by another identity
|
||||
const uuidMatch = mappings.find(m => m.uuid.toLowerCase() === uuid.toLowerCase());
|
||||
if (uuidMatch) {
|
||||
showNotification(`This UUID is already used by identity "${uuidMatch.username}". Each identity must have a unique UUID.`, 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check username reservation on auth server
|
||||
try {
|
||||
const cfg = await window.electronAPI.loadConfig();
|
||||
const authDomain = cfg.authDomain || 'auth.sanasol.ws';
|
||||
const checkResp = await fetch(`https://${authDomain}/player/username/status/${encodeURIComponent(username)}`);
|
||||
if (checkResp.ok) {
|
||||
const status = await checkResp.json();
|
||||
if (status.reserved) {
|
||||
showNotification(`Username "${username}" is reserved by another player who set a password. Choose a different name.`, 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Server check failed — allow creation (fail-open)
|
||||
console.log('[Identity] Server username check skipped:', e.message);
|
||||
}
|
||||
|
||||
// Check if UUID is password-protected on server (restore access flow)
|
||||
let uuidIsProtected = false;
|
||||
let registeredName = null;
|
||||
try {
|
||||
if (window.electronAPI.checkPasswordStatus) {
|
||||
const pwStatus = await window.electronAPI.checkPasswordStatus(uuid);
|
||||
if (pwStatus && pwStatus.hasPassword) {
|
||||
uuidIsProtected = true;
|
||||
registeredName = pwStatus.registeredName || null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Identity] UUID password check skipped:', e.message);
|
||||
}
|
||||
|
||||
if (uuidIsProtected) {
|
||||
// UUID is password-protected — need password to restore it
|
||||
showRestoreProtectedIdentityDialog(username, uuid, registeredName);
|
||||
return;
|
||||
}
|
||||
|
||||
await saveNewIdentity(username, uuid);
|
||||
} catch (error) {
|
||||
console.error('Error adding identity:', error);
|
||||
const msg = window.i18n ? window.i18n.t('notifications.identityAddFailed') : 'Failed to add identity';
|
||||
@@ -931,6 +976,174 @@ async function confirmAddIdentity() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveNewIdentity(username, uuid) {
|
||||
if (window.electronAPI && window.electronAPI.setUuidForUser) {
|
||||
const result = await window.electronAPI.setUuidForUser(username, uuid);
|
||||
if (result.success) {
|
||||
showNotification('Identity added successfully!', 'success');
|
||||
hideAddIdentityForm();
|
||||
await loadAllUuids();
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
updatePasswordShieldIcon();
|
||||
} else if (result.error === 'duplicate') {
|
||||
showNotification(`Identity "${username}" already exists (UUID: ${result.existingUuid.substring(0, 8)}...). Use the identity list to manage it.`, 'error');
|
||||
} else if (result.error === 'uuid_in_use') {
|
||||
showNotification(`This UUID is already used by identity "${result.existingUsername}". Each identity must have a unique UUID.`, 'error');
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to add identity');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showRestoreProtectedIdentityDialog(username, uuid, registeredName) {
|
||||
const existing = document.querySelector('.custom-confirm-modal');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const nameWarning = registeredName && registeredName.toLowerCase() !== username.toLowerCase()
|
||||
? `<p style="color: #f59e0b; margin: 0 0 12px; font-size: 0.9rem;">
|
||||
<i class="fas fa-exclamation-triangle"></i> This UUID is locked to name "<strong>${escapeHtml(registeredName)}</strong>".
|
||||
Your entered name "${escapeHtml(username)}" will be replaced.
|
||||
</p>`
|
||||
: '';
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'custom-confirm-modal';
|
||||
overlay.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.style.cssText = `
|
||||
background: #1f2937; border-radius: 12px; padding: 0;
|
||||
min-width: 420px; max-width: 520px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
|
||||
border: 1px solid rgba(147, 51, 234, 0.4);
|
||||
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-shield-alt" style="font-size: 24px;"></i>
|
||||
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">Restore Protected Identity</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 24px;">
|
||||
<p style="color: #e5e7eb; margin: 0 0 16px; line-height: 1.6;">
|
||||
This UUID is <strong style="color: #22c55e;">password-protected</strong>. Enter the password to restore access.
|
||||
</p>
|
||||
${nameWarning}
|
||||
<div id="restoreError" style="display: none; color: #f87171; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px; padding: 8px 12px; margin-bottom: 12px; font-size: 0.85rem;"></div>
|
||||
<input type="password" id="restorePasswordInput" style="
|
||||
width: 100%; box-sizing: border-box; padding: 10px 14px;
|
||||
background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 8px; color: #fff; font-size: 0.95rem; outline: none;
|
||||
" placeholder="Password" autofocus />
|
||||
</div>
|
||||
<div style="padding: 16px 24px; display: flex; gap: 10px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
<button id="restoreCancelBtn" style="
|
||||
padding: 8px 20px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.2);
|
||||
background: transparent; color: #9ca3af; cursor: pointer; font-size: 0.9rem;
|
||||
">Cancel</button>
|
||||
<button id="restoreConfirmBtn" style="
|
||||
padding: 8px 20px; border-radius: 8px; border: none;
|
||||
background: linear-gradient(135deg, #9333ea, #3b82f6); color: white;
|
||||
cursor: pointer; font-weight: 600; font-size: 0.9rem;
|
||||
">Verify & Restore</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.appendChild(dialog);
|
||||
document.body.appendChild(overlay);
|
||||
requestAnimationFrame(() => {
|
||||
overlay.style.opacity = '1';
|
||||
dialog.style.transform = 'scale(1)';
|
||||
});
|
||||
|
||||
const input = overlay.querySelector('#restorePasswordInput');
|
||||
const errorMsg = overlay.querySelector('#restoreError');
|
||||
const confirmBtn = overlay.querySelector('#restoreConfirmBtn');
|
||||
const cancelBtn = overlay.querySelector('#restoreCancelBtn');
|
||||
let busy = false;
|
||||
|
||||
const close = () => { overlay.remove(); };
|
||||
cancelBtn.onclick = close;
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
||||
|
||||
const doRestore = async () => {
|
||||
if (busy) return;
|
||||
const password = input.value.trim();
|
||||
if (!password) {
|
||||
errorMsg.textContent = 'Password is required';
|
||||
errorMsg.style.display = 'block';
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
busy = true;
|
||||
confirmBtn.disabled = true;
|
||||
confirmBtn.textContent = 'Verifying...';
|
||||
errorMsg.style.display = 'none';
|
||||
|
||||
try {
|
||||
// Use the registered name if UUID is name-locked
|
||||
const finalName = registeredName || username;
|
||||
|
||||
// Verify password by attempting to get tokens
|
||||
const cfg = await window.electronAPI.loadConfig();
|
||||
const authDomain = cfg.authDomain || 'auth.sanasol.ws';
|
||||
const resp = await fetch(`https://${authDomain}/game-session/new`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ uuid, name: finalName, password, scopes: 'hytale:server hytale:client' })
|
||||
});
|
||||
|
||||
if (resp.status === 401 || resp.status === 429) {
|
||||
const err = await resp.json();
|
||||
errorMsg.textContent = err.error || 'Incorrect password';
|
||||
errorMsg.style.display = 'block';
|
||||
input.value = '';
|
||||
input.focus();
|
||||
busy = false;
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.textContent = 'Verify & Restore';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Server returned ${resp.status}`);
|
||||
}
|
||||
|
||||
// Password verified — save identity locally (force to allow the name)
|
||||
close();
|
||||
if (window.electronAPI && window.electronAPI.setUuidForUser) {
|
||||
const result = await window.electronAPI.setUuidForUser(finalName, uuid, true);
|
||||
if (result.success || (result && result.uuid)) {
|
||||
showNotification(`Identity "${finalName}" restored successfully!`, 'success');
|
||||
hideAddIdentityForm();
|
||||
await loadAllUuids();
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
updatePasswordShieldIcon();
|
||||
} else {
|
||||
showNotification(result.error || 'Failed to save identity', 'error');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
errorMsg.textContent = 'Error: ' + e.message;
|
||||
errorMsg.style.display = 'block';
|
||||
busy = false;
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.textContent = 'Verify & Restore';
|
||||
}
|
||||
};
|
||||
|
||||
confirmBtn.onclick = doRestore;
|
||||
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doRestore(); });
|
||||
}
|
||||
|
||||
function toggleAdvancedSection() {
|
||||
if (!uuidAdvancedContent || !uuidAdvancedToggle) return;
|
||||
const isOpen = uuidAdvancedContent.style.display !== 'none';
|
||||
@@ -941,6 +1154,250 @@ function toggleAdvancedSection() {
|
||||
}
|
||||
}
|
||||
|
||||
// Password section toggle
|
||||
function togglePasswordSection() {
|
||||
const content = document.getElementById('passwordSectionContent');
|
||||
const toggle = document.getElementById('passwordSectionToggle');
|
||||
if (!content || !toggle) return;
|
||||
const isOpen = content.style.display !== 'none';
|
||||
content.style.display = isOpen ? 'none' : 'block';
|
||||
const chevron = toggle.querySelector('.uuid-advanced-chevron');
|
||||
if (chevron) chevron.classList.toggle('open', !isOpen);
|
||||
if (!isOpen) refreshPasswordStatus();
|
||||
}
|
||||
|
||||
async function refreshPasswordStatus() {
|
||||
const statusMsg = document.getElementById('passwordStatusMsg');
|
||||
const currentPwInput = document.getElementById('currentPasswordInput');
|
||||
const removeBtn = document.getElementById('removePasswordBtn');
|
||||
const setBtn = document.getElementById('setPasswordBtn');
|
||||
try {
|
||||
const uuid = await window.electronAPI?.getCurrentUuid();
|
||||
if (!uuid) { if (statusMsg) statusMsg.textContent = 'No UUID available'; return; }
|
||||
const result = await window.electronAPI.checkPasswordStatus(uuid);
|
||||
if (result && result.hasPassword) {
|
||||
if (statusMsg) statusMsg.innerHTML = '<i class="fas fa-lock" style="color:#22c55e"></i> Password is set for this UUID';
|
||||
if (currentPwInput) currentPwInput.style.display = '';
|
||||
if (removeBtn) removeBtn.style.display = '';
|
||||
if (setBtn) { const span = setBtn.querySelector('span'); if (span) span.textContent = 'Change Password'; }
|
||||
} else {
|
||||
if (statusMsg) statusMsg.innerHTML = '<i class="fas fa-unlock" style="color:#f59e0b"></i> No password set — anyone can use this UUID';
|
||||
if (currentPwInput) currentPwInput.style.display = 'none';
|
||||
if (removeBtn) removeBtn.style.display = 'none';
|
||||
if (setBtn) { const span = setBtn.querySelector('span'); if (span) span.textContent = 'Set Password'; }
|
||||
}
|
||||
} catch (e) {
|
||||
if (statusMsg) statusMsg.textContent = 'Could not check password status';
|
||||
}
|
||||
}
|
||||
|
||||
window.handleSetPassword = async function () {
|
||||
const newPw = document.getElementById('newPasswordInput');
|
||||
const currentPw = document.getElementById('currentPasswordInput');
|
||||
const setBtn = document.getElementById('setPasswordBtn');
|
||||
if (!newPw || !newPw.value || newPw.value.length < 6) {
|
||||
showNotification('Password must be at least 6 characters', 'error');
|
||||
return;
|
||||
}
|
||||
if (setBtn) { setBtn.disabled = true; setBtn.textContent = 'Setting...'; }
|
||||
try {
|
||||
const uuid = await window.electronAPI.getCurrentUuid();
|
||||
const result = await window.electronAPI.setPlayerPassword(uuid, newPw.value, currentPw?.value || null);
|
||||
if (result.success) {
|
||||
showNotification('Password set successfully', 'success');
|
||||
newPw.value = '';
|
||||
if (currentPw) currentPw.value = '';
|
||||
refreshPasswordStatus();
|
||||
updatePasswordShieldIcon();
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
} else {
|
||||
showNotification(result.error || 'Failed to set password', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Error: ' + e.message, 'error');
|
||||
} finally {
|
||||
if (setBtn) { setBtn.disabled = false; setBtn.textContent = 'Set Password'; }
|
||||
}
|
||||
};
|
||||
|
||||
window.handleRemovePassword = async function () {
|
||||
const currentPw = document.getElementById('currentPasswordInput');
|
||||
const removeBtn = document.getElementById('removePasswordBtn');
|
||||
if (!currentPw || !currentPw.value) {
|
||||
showNotification('Enter your current password to remove it', 'error');
|
||||
return;
|
||||
}
|
||||
if (removeBtn) { removeBtn.disabled = true; removeBtn.textContent = 'Removing...'; }
|
||||
try {
|
||||
const uuid = await window.electronAPI.getCurrentUuid();
|
||||
const result = await window.electronAPI.removePlayerPassword(uuid, currentPw.value);
|
||||
if (result.success) {
|
||||
showNotification('Password removed', 'success');
|
||||
currentPw.value = '';
|
||||
refreshPasswordStatus();
|
||||
updatePasswordShieldIcon();
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
} else {
|
||||
showNotification(result.error || 'Failed to remove password', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Error: ' + e.message, 'error');
|
||||
} finally {
|
||||
if (removeBtn) { removeBtn.disabled = false; removeBtn.textContent = 'Remove Password'; }
|
||||
}
|
||||
};
|
||||
|
||||
// ─── Password Shield Icon ───
|
||||
|
||||
window.updatePasswordShieldIcon = updatePasswordShieldIcon;
|
||||
async function updatePasswordShieldIcon() {
|
||||
const icon = document.getElementById('passwordShieldIcon');
|
||||
if (!icon) return;
|
||||
try {
|
||||
const uuid = await window.electronAPI?.getCurrentUuid();
|
||||
if (!uuid) {
|
||||
icon.className = 'fas fa-unlock password-shield unprotected';
|
||||
icon.setAttribute('data-tooltip', 'No identity loaded');
|
||||
return;
|
||||
}
|
||||
const result = await window.electronAPI.checkPasswordStatus(uuid);
|
||||
if (result && result.hasPassword) {
|
||||
icon.className = 'fas fa-lock password-shield protected';
|
||||
icon.setAttribute('data-tooltip', 'Protected — click to manage');
|
||||
} else {
|
||||
icon.className = 'fas fa-unlock password-shield unprotected';
|
||||
icon.setAttribute('data-tooltip', 'Click to protect identity');
|
||||
}
|
||||
} catch (e) {
|
||||
icon.className = 'fas fa-unlock password-shield unprotected';
|
||||
icon.setAttribute('data-tooltip', 'Click to protect identity');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Password Modal ───
|
||||
|
||||
window.openPasswordModal = async function () {
|
||||
const modal = document.getElementById('passwordModal');
|
||||
if (!modal) return;
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// Clear inputs
|
||||
const newPw = document.getElementById('pwModalNewPassword');
|
||||
const curPw = document.getElementById('pwModalCurrentPassword');
|
||||
if (newPw) newPw.value = '';
|
||||
if (curPw) curPw.value = '';
|
||||
|
||||
// Load current identity info
|
||||
try {
|
||||
const username = await window.electronAPI?.loadUsername() || 'Player';
|
||||
const uuid = await window.electronAPI?.getCurrentUuid() || '';
|
||||
document.getElementById('pwModalName').textContent = username;
|
||||
document.getElementById('pwModalUuid').textContent = uuid || 'No UUID';
|
||||
|
||||
// Check password status
|
||||
const result = uuid ? await window.electronAPI.checkPasswordStatus(uuid) : null;
|
||||
const badge = document.getElementById('pwModalStatusBadge');
|
||||
const statusText = document.getElementById('pwModalStatusText');
|
||||
const curPwInput = document.getElementById('pwModalCurrentPassword');
|
||||
const removeBtn = document.getElementById('pwModalRemoveBtn');
|
||||
const setBtn = document.getElementById('pwModalSetBtn');
|
||||
const usernameInfo = document.getElementById('pwModalUsernameInfo');
|
||||
|
||||
if (result && result.hasPassword) {
|
||||
badge.innerHTML = '<span style="color:#22c55e;font-size:0.8em;padding:3px 8px;background:rgba(34,197,94,0.15);border-radius:6px;"><i class="fas fa-lock"></i> Protected</span>';
|
||||
statusText.innerHTML = '<i class="fas fa-check-circle" style="color:#22c55e"></i> Password set — your UUID and username are protected';
|
||||
if (curPwInput) curPwInput.style.display = '';
|
||||
if (removeBtn) removeBtn.style.display = '';
|
||||
if (setBtn) { const s = setBtn.querySelector('span'); if (s) s.textContent = 'Change Password'; }
|
||||
if (usernameInfo) usernameInfo.style.display = 'none';
|
||||
} else {
|
||||
badge.innerHTML = '<span style="color:#f59e0b;font-size:0.8em;padding:3px 8px;background:rgba(245,158,11,0.1);border-radius:6px;"><i class="fas fa-unlock"></i> Open</span>';
|
||||
statusText.innerHTML = '<i class="fas fa-exclamation-triangle" style="color:#f59e0b"></i> Anyone can use this UUID and username';
|
||||
if (curPwInput) curPwInput.style.display = 'none';
|
||||
if (removeBtn) removeBtn.style.display = 'none';
|
||||
if (setBtn) { const s = setBtn.querySelector('span'); if (s) s.textContent = 'Set Password'; }
|
||||
if (usernameInfo) usernameInfo.style.display = '';
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('pwModalStatusText').textContent = 'Could not check password status';
|
||||
}
|
||||
};
|
||||
|
||||
window.closePasswordModal = function () {
|
||||
const modal = document.getElementById('passwordModal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
};
|
||||
|
||||
window.handlePasswordModalSet = async function () {
|
||||
const newPw = document.getElementById('pwModalNewPassword');
|
||||
const curPw = document.getElementById('pwModalCurrentPassword');
|
||||
const setBtn = document.getElementById('pwModalSetBtn');
|
||||
if (!newPw || !newPw.value || newPw.value.length < 6) {
|
||||
showNotification('Password must be at least 6 characters', 'error');
|
||||
return;
|
||||
}
|
||||
if (setBtn) { setBtn.disabled = true; const s = setBtn.querySelector('span'); if (s) s.textContent = 'Saving...'; }
|
||||
try {
|
||||
const uuid = await window.electronAPI.getCurrentUuid();
|
||||
const result = await window.electronAPI.setPlayerPassword(uuid, newPw.value, curPw?.value || null);
|
||||
if (result.success) {
|
||||
const msg = result.username_reserved ? 'Password set! Username "' + (result.reserved_username || '') + '" reserved.' : 'Password set!';
|
||||
showNotification(msg, 'success');
|
||||
newPw.value = '';
|
||||
if (curPw) curPw.value = '';
|
||||
updatePasswordShieldIcon();
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
openPasswordModal(); // refresh modal state
|
||||
} else {
|
||||
showNotification(result.error || 'Failed to set password', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Error: ' + e.message, 'error');
|
||||
} finally {
|
||||
if (setBtn) { setBtn.disabled = false; const s = setBtn.querySelector('span'); if (s) s.textContent = 'Set Password'; }
|
||||
}
|
||||
};
|
||||
|
||||
window.handlePasswordModalRemove = async function () {
|
||||
const curPw = document.getElementById('pwModalCurrentPassword');
|
||||
const removeBtn = document.getElementById('pwModalRemoveBtn');
|
||||
if (!curPw || !curPw.value) {
|
||||
showNotification('Enter your current password to remove it', 'error');
|
||||
return;
|
||||
}
|
||||
if (removeBtn) { removeBtn.disabled = true; removeBtn.textContent = 'Removing...'; }
|
||||
try {
|
||||
const uuid = await window.electronAPI.getCurrentUuid();
|
||||
const result = await window.electronAPI.removePlayerPassword(uuid, curPw.value);
|
||||
if (result.success) {
|
||||
showNotification('Password removed', 'success');
|
||||
curPw.value = '';
|
||||
updatePasswordShieldIcon();
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
openPasswordModal(); // refresh modal state
|
||||
} else {
|
||||
showNotification(result.error || 'Failed to remove password', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showNotification('Error: ' + e.message, 'error');
|
||||
} finally {
|
||||
if (removeBtn) { removeBtn.disabled = false; removeBtn.textContent = 'Remove Password'; }
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal on backdrop click
|
||||
document.addEventListener('click', (e) => {
|
||||
const modal = document.getElementById('passwordModal');
|
||||
if (modal && e.target === modal) closePasswordModal();
|
||||
});
|
||||
|
||||
// Bind password section toggle (for legacy UUID modal section)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const pwToggle = document.getElementById('passwordSectionToggle');
|
||||
if (pwToggle) pwToggle.addEventListener('click', togglePasswordSection);
|
||||
updatePasswordShieldIcon();
|
||||
});
|
||||
|
||||
window.regenerateUuidForUser = async function (username) {
|
||||
try {
|
||||
const message = window.i18n ? window.i18n.t('confirm.regenerateUuidMessage') : 'Are you sure you want to generate a new UUID? This will change your player identity.';
|
||||
@@ -1016,7 +1473,7 @@ async function performSetCustomUuid(uuid) {
|
||||
showNotification(msg, 'error');
|
||||
return;
|
||||
}
|
||||
const result = await window.electronAPI.setUuidForUser(username, uuid);
|
||||
const result = await window.electronAPI.setUuidForUser(username, uuid, true); // force: true — explicit UUID change
|
||||
|
||||
if (result.success) {
|
||||
if (currentUuidDisplay) currentUuidDisplay.value = uuid;
|
||||
@@ -1027,6 +1484,7 @@ async function performSetCustomUuid(uuid) {
|
||||
|
||||
await loadAllUuids();
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
updatePasswordShieldIcon();
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to set custom UUID');
|
||||
}
|
||||
@@ -1105,8 +1563,9 @@ async function performSwitchToUsername(username) {
|
||||
// Refresh the UUID list to show new "Current" badge
|
||||
await loadAllUuids();
|
||||
|
||||
// Refresh header identity dropdown
|
||||
// Refresh header identity dropdown + shield icon
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
updatePasswordShieldIcon();
|
||||
|
||||
const msg = window.i18n
|
||||
? window.i18n.t('notifications.switchUsernameSuccess').replace('{username}', username)
|
||||
@@ -1124,46 +1583,195 @@ async function performSwitchToUsername(username) {
|
||||
|
||||
window.deleteUuid = async function (username) {
|
||||
try {
|
||||
const message = window.i18n ? window.i18n.t('confirm.deleteUuidMessage').replace('{username}', username) : `Are you sure you want to delete the UUID for "${username}"? This action cannot be undone.`;
|
||||
const title = window.i18n ? window.i18n.t('confirm.deleteUuidTitle') : 'Delete UUID';
|
||||
const confirmBtn = window.i18n ? window.i18n.t('confirm.deleteUuidButton') : 'Delete';
|
||||
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||
// Look up UUID for this username
|
||||
let uuid = null;
|
||||
if (window.electronAPI && window.electronAPI.getAllUuidMappings) {
|
||||
const mappings = await window.electronAPI.getAllUuidMappings();
|
||||
const entry = mappings.find(m => m.username.toLowerCase() === username.toLowerCase());
|
||||
if (entry) uuid = entry.uuid;
|
||||
}
|
||||
|
||||
// Check if password-protected
|
||||
let isProtected = false;
|
||||
if (uuid && window.electronAPI && window.electronAPI.checkPasswordStatus) {
|
||||
try {
|
||||
const pwStatus = await window.electronAPI.checkPasswordStatus(uuid);
|
||||
isProtected = pwStatus && pwStatus.hasPassword;
|
||||
} catch (e) {
|
||||
console.log('[Identity] Password status check failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (isProtected) {
|
||||
// Password-protected identity — show warning with password input
|
||||
showPasswordProtectedDeleteDialog(username, uuid);
|
||||
} else {
|
||||
// Normal identity — simple confirm
|
||||
const message = `Are you sure you want to delete the identity "${username}"? This action cannot be undone.`;
|
||||
showCustomConfirm(
|
||||
message,
|
||||
title,
|
||||
async () => {
|
||||
await performDeleteUuid(username);
|
||||
},
|
||||
'Delete Identity',
|
||||
async () => { await performDeleteUuid(username); },
|
||||
null,
|
||||
confirmBtn,
|
||||
cancelBtn
|
||||
'Delete',
|
||||
'Cancel'
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in deleteUuid:', error);
|
||||
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed') : 'Failed to delete UUID';
|
||||
showNotification(msg, 'error');
|
||||
showNotification('Failed to delete identity', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
function showPasswordProtectedDeleteDialog(username, uuid) {
|
||||
const existing = document.querySelector('.custom-confirm-modal');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'custom-confirm-modal';
|
||||
overlay.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.style.cssText = `
|
||||
background: #1f2937; border-radius: 12px; padding: 0;
|
||||
min-width: 420px; max-width: 520px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.6);
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
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-shield-alt" style="font-size: 24px;"></i>
|
||||
<h3 style="margin: 0; font-size: 1.2rem; font-weight: 600;">Delete Protected Identity</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 24px;">
|
||||
<p style="color: #e5e7eb; margin: 0 0 16px; line-height: 1.6;">
|
||||
<strong>"${escapeHtml(username)}"</strong> is password-protected. Deleting it will:
|
||||
</p>
|
||||
<ul style="color: #f87171; margin: 0 0 16px; padding-left: 20px; line-height: 1.8;">
|
||||
<li>Remove the password protection from this UUID</li>
|
||||
<li>Release the reserved username "${escapeHtml(username)}"</li>
|
||||
<li>Allow anyone to use this UUID and name</li>
|
||||
</ul>
|
||||
<p style="color: #9ca3af; margin: 0 0 16px; font-size: 0.9rem;">
|
||||
Enter your current password to confirm deletion:
|
||||
</p>
|
||||
<div id="pwDeleteError" style="display: none; color: #f87171; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px; padding: 8px 12px; margin-bottom: 12px; font-size: 0.85rem;"></div>
|
||||
<input type="password" id="pwDeleteInput" style="
|
||||
width: 100%; box-sizing: border-box; padding: 10px 14px;
|
||||
background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 8px; color: #fff; font-size: 0.95rem; outline: none;
|
||||
" placeholder="Current password" autofocus />
|
||||
</div>
|
||||
<div style="padding: 16px 24px; display: flex; gap: 10px; justify-content: flex-end; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
<button id="pwDeleteCancelBtn" style="
|
||||
padding: 8px 20px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.2);
|
||||
background: transparent; color: #9ca3af; cursor: pointer; font-size: 0.9rem;
|
||||
">Cancel</button>
|
||||
<button id="pwDeleteConfirmBtn" style="
|
||||
padding: 8px 20px; border-radius: 8px; border: none;
|
||||
background: #ef4444; color: white; cursor: pointer; font-weight: 600; font-size: 0.9rem;
|
||||
">Delete & Remove Password</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.appendChild(dialog);
|
||||
document.body.appendChild(overlay);
|
||||
requestAnimationFrame(() => {
|
||||
overlay.style.opacity = '1';
|
||||
dialog.style.transform = 'scale(1)';
|
||||
});
|
||||
|
||||
const input = overlay.querySelector('#pwDeleteInput');
|
||||
const errorMsg = overlay.querySelector('#pwDeleteError');
|
||||
const confirmBtn = overlay.querySelector('#pwDeleteConfirmBtn');
|
||||
const cancelBtn = overlay.querySelector('#pwDeleteCancelBtn');
|
||||
let busy = false;
|
||||
|
||||
const close = () => { overlay.remove(); };
|
||||
|
||||
cancelBtn.onclick = close;
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
||||
|
||||
const doDelete = async () => {
|
||||
if (busy) return;
|
||||
const password = input.value.trim();
|
||||
if (!password) {
|
||||
errorMsg.textContent = 'Password is required';
|
||||
errorMsg.style.display = 'block';
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
busy = true;
|
||||
confirmBtn.disabled = true;
|
||||
confirmBtn.textContent = 'Removing...';
|
||||
errorMsg.style.display = 'none';
|
||||
|
||||
try {
|
||||
// Step 1: Remove password on server (validates current password)
|
||||
const removeResult = await window.electronAPI.removePlayerPassword(uuid, password);
|
||||
if (!removeResult.success) {
|
||||
errorMsg.textContent = removeResult.error || 'Incorrect password';
|
||||
errorMsg.style.display = 'block';
|
||||
input.value = '';
|
||||
input.focus();
|
||||
busy = false;
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.textContent = 'Delete & Remove Password';
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Also clear saved password if any
|
||||
try {
|
||||
const cfg = await window.electronAPI.loadConfig();
|
||||
if (cfg.savedPasswords && cfg.savedPasswords[uuid]) {
|
||||
delete cfg.savedPasswords[uuid];
|
||||
await window.electronAPI.saveConfig({ savedPasswords: cfg.savedPasswords });
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// Step 3: Delete identity locally
|
||||
close();
|
||||
await performDeleteUuid(username);
|
||||
} catch (e) {
|
||||
errorMsg.textContent = 'Error: ' + e.message;
|
||||
errorMsg.style.display = 'block';
|
||||
busy = false;
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.textContent = 'Delete & Remove Password';
|
||||
}
|
||||
};
|
||||
|
||||
confirmBtn.onclick = doDelete;
|
||||
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doDelete(); });
|
||||
}
|
||||
|
||||
async function performDeleteUuid(username) {
|
||||
try {
|
||||
if (window.electronAPI && window.electronAPI.deleteUuidForUser) {
|
||||
const result = await window.electronAPI.deleteUuidForUser(username);
|
||||
|
||||
if (result.success) {
|
||||
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteSuccess') : 'UUID deleted successfully!';
|
||||
showNotification(msg, 'success');
|
||||
showNotification('Identity deleted successfully!', 'success');
|
||||
await loadAllUuids();
|
||||
if (window.loadIdentities) window.loadIdentities();
|
||||
updatePasswordShieldIcon();
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to delete UUID');
|
||||
throw new Error(result.error || 'Failed to delete identity');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting UUID:', error);
|
||||
const msg = window.i18n ? window.i18n.t('notifications.uuidDeleteFailed').replace('{error}', error.message) : `Failed to delete UUID: ${error.message}`;
|
||||
showNotification(msg, 'error');
|
||||
showNotification(`Failed to delete identity: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1316,6 +1924,11 @@ async function switchBranch(newBranch) {
|
||||
await loadVersionBranch();
|
||||
console.log('[Settings] Radio buttons updated after branch switch');
|
||||
|
||||
// Refresh game info bar on play page
|
||||
if (window.LauncherUI?.loadGameInfoBar) {
|
||||
window.LauncherUI.loadGameInfoBar();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (window.LauncherUI) {
|
||||
window.LauncherUI.hideProgress();
|
||||
|
||||
59
GUI/js/ui.js
59
GUI/js/ui.js
@@ -138,6 +138,7 @@ function showLauncherOrInstall(isInstalled) {
|
||||
if (gameTitle) gameTitle.style.display = '';
|
||||
showPage('play-page');
|
||||
setActiveNav('play');
|
||||
loadGameInfoBar();
|
||||
} else {
|
||||
if (launcher) launcher.style.display = 'none';
|
||||
if (install) {
|
||||
@@ -738,13 +739,69 @@ async function checkGameInstallation() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGameInfoBar() {
|
||||
try {
|
||||
const info = await window.electronAPI.getGameInfo();
|
||||
const bar = document.getElementById('game-info-bar');
|
||||
if (!bar) return;
|
||||
|
||||
const versionEl = document.getElementById('game-version-info');
|
||||
const branchEl = document.getElementById('game-branch-info');
|
||||
const lastPlayedEl = document.getElementById('game-last-played');
|
||||
|
||||
if (versionEl) {
|
||||
versionEl.classList.remove('game-info-loading');
|
||||
if (info.version) {
|
||||
const display = info.readableVersion
|
||||
? `${info.version} - ${info.readableVersion}`
|
||||
: info.version;
|
||||
versionEl.textContent = display;
|
||||
} else {
|
||||
versionEl.textContent = 'Not installed';
|
||||
}
|
||||
}
|
||||
if (branchEl) {
|
||||
branchEl.classList.remove('game-info-loading');
|
||||
const isPreRelease = info.branch === 'pre-release';
|
||||
branchEl.textContent = isPreRelease ? 'Pre-Release' : 'Release';
|
||||
branchEl.style.color = isPreRelease ? '#f59e0b' : '';
|
||||
}
|
||||
|
||||
if (lastPlayedEl && info.lastPlayed) {
|
||||
lastPlayedEl.style.display = '';
|
||||
if (!lastPlayedEl.previousElementSibling?.classList?.contains('game-info-sep')) {
|
||||
const sep = document.createElement('span');
|
||||
sep.className = 'game-info-sep';
|
||||
lastPlayedEl.before(sep);
|
||||
}
|
||||
lastPlayedEl.textContent = formatTimeAgo(info.lastPlayed);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not load game info:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimeAgo(timestamp) {
|
||||
const diff = Date.now() - timestamp;
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return 'Just played';
|
||||
if (minutes < 60) return `Played ${minutes}m ago`;
|
||||
if (hours < 24) return `Played ${hours}h ago`;
|
||||
if (days < 30) return `Played ${days}d ago`;
|
||||
return `Played ${Math.floor(days / 30)}mo ago`;
|
||||
}
|
||||
|
||||
window.LauncherUI = {
|
||||
showPage,
|
||||
setActiveNav,
|
||||
showLauncherOrInstall,
|
||||
showProgress,
|
||||
hideProgress,
|
||||
updateProgress
|
||||
updateProgress,
|
||||
loadGameInfoBar
|
||||
};
|
||||
|
||||
// Make installation effects globally available
|
||||
|
||||
@@ -16,6 +16,8 @@ class ClientUpdateManager {
|
||||
});
|
||||
|
||||
window.electronAPI.onUpdateDownloadProgress((progress) => {
|
||||
console.log('📊 download-progress event:', progress);
|
||||
this.downloadProgressReceived = true;
|
||||
this.updateDownloadProgress(progress);
|
||||
});
|
||||
|
||||
@@ -49,7 +51,7 @@ class ClientUpdateManager {
|
||||
|
||||
const popupHTML = `
|
||||
<div id="update-popup-overlay">
|
||||
<div class="update-popup-container update-popup-pulse">
|
||||
<div class="update-popup-container">
|
||||
<div class="update-popup-header">
|
||||
<div class="update-popup-icon">
|
||||
<i class="fas fa-download"></i>
|
||||
@@ -118,11 +120,29 @@ class ClientUpdateManager {
|
||||
this.blockInterface();
|
||||
|
||||
// Show progress container immediately (auto-download is enabled)
|
||||
this.downloadProgressReceived = false;
|
||||
const progressContainer = document.getElementById('update-progress-container');
|
||||
if (progressContainer) {
|
||||
progressContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
// If no progress events arrive within 2 seconds, show indeterminate animation
|
||||
setTimeout(() => {
|
||||
if (!this.downloadProgressReceived && progressContainer) {
|
||||
const bar = document.getElementById('update-progress-bar');
|
||||
const speed = document.getElementById('update-progress-speed');
|
||||
const percent = document.getElementById('update-progress-percent');
|
||||
if (bar) {
|
||||
bar.style.width = '100%';
|
||||
bar.style.animation = 'indeterminateProgress 1.5s ease-in-out infinite';
|
||||
bar.style.opacity = '0.7';
|
||||
}
|
||||
if (speed) speed.textContent = '';
|
||||
if (percent) percent.textContent = 'Downloading...';
|
||||
console.log('⏳ No download-progress events received, showing indeterminate progress');
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
const installBtn = document.getElementById('update-install-btn');
|
||||
if (installBtn) {
|
||||
installBtn.addEventListener('click', async (e) => {
|
||||
@@ -248,6 +268,10 @@ class ClientUpdateManager {
|
||||
const progressSize = document.getElementById('update-progress-size');
|
||||
|
||||
if (progressBar && progress) {
|
||||
// Stop indeterminate animation if it was running
|
||||
progressBar.style.animation = 'none';
|
||||
progressBar.style.opacity = '1';
|
||||
|
||||
const percent = Math.round(progress.percent || 0);
|
||||
progressBar.style.width = `${percent}%`;
|
||||
|
||||
@@ -273,50 +297,29 @@ class ClientUpdateManager {
|
||||
showUpdateDownloaded(updateInfo) {
|
||||
const statusText = document.getElementById('update-status-text');
|
||||
const progressContainer = document.getElementById('update-progress-container');
|
||||
const progressBar = document.getElementById('update-progress-bar');
|
||||
const progressPercent = document.getElementById('update-progress-percent');
|
||||
const progressSpeed = document.getElementById('update-progress-speed');
|
||||
const progressSize = document.getElementById('update-progress-size');
|
||||
const buttonsContainer = document.getElementById('update-buttons-container');
|
||||
const installBtn = document.getElementById('update-install-btn');
|
||||
const downloadBtn = document.getElementById('update-download-btn');
|
||||
const skipBtn = document.getElementById('update-skip-btn');
|
||||
const footerText = document.getElementById('update-footer-text');
|
||||
const popupContainer = document.querySelector('.update-popup-container');
|
||||
|
||||
// Remove breathing/pulse animation when download is complete
|
||||
if (popupContainer) {
|
||||
popupContainer.classList.remove('update-popup-pulse');
|
||||
// Stop indeterminate animation and show 100%
|
||||
if (progressBar) {
|
||||
progressBar.style.animation = 'none';
|
||||
progressBar.style.opacity = '1';
|
||||
progressBar.style.width = '100%';
|
||||
}
|
||||
if (progressPercent) progressPercent.textContent = '100%';
|
||||
if (progressSpeed) progressSpeed.textContent = 'Complete';
|
||||
if (progressContainer) progressContainer.style.display = 'block';
|
||||
|
||||
if (progressContainer) {
|
||||
progressContainer.style.display = 'none';
|
||||
}
|
||||
// Hide progress after a short delay so user sees 100%
|
||||
setTimeout(() => {
|
||||
if (progressContainer) progressContainer.style.display = 'none';
|
||||
}, 1500);
|
||||
|
||||
// Use platform info from main process if available, fallback to browser detection
|
||||
const autoInstallSupported = updateInfo.autoInstallSupported !== undefined
|
||||
? updateInfo.autoInstallSupported
|
||||
: navigator.platform.toUpperCase().indexOf('MAC') < 0;
|
||||
|
||||
if (!autoInstallSupported) {
|
||||
// macOS: Show manual download as primary since auto-update doesn't work
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Update downloaded but auto-install may not work on macOS.';
|
||||
}
|
||||
|
||||
if (installBtn) {
|
||||
// Still show install button but as secondary option
|
||||
installBtn.classList.add('update-download-btn-secondary');
|
||||
installBtn.innerHTML = '<i class="fas fa-check" style="margin-right: 0.5rem;"></i>Try Install & Restart';
|
||||
}
|
||||
|
||||
if (downloadBtn) {
|
||||
// Make manual download primary
|
||||
downloadBtn.classList.remove('update-download-btn-secondary');
|
||||
downloadBtn.innerHTML = '<i class="fas fa-external-link-alt" style="margin-right: 0.5rem;"></i>Download Manually (Recommended)';
|
||||
}
|
||||
|
||||
if (footerText) {
|
||||
footerText.textContent = 'Auto-install often fails on macOS:';
|
||||
}
|
||||
} else {
|
||||
// Windows/Linux: Auto-install should work
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Update downloaded! Ready to install.';
|
||||
}
|
||||
@@ -324,7 +327,6 @@ class ClientUpdateManager {
|
||||
if (footerText) {
|
||||
footerText.textContent = 'Click to install the update:';
|
||||
}
|
||||
}
|
||||
|
||||
if (buttonsContainer) {
|
||||
buttonsContainer.style.display = 'block';
|
||||
@@ -338,7 +340,7 @@ class ClientUpdateManager {
|
||||
console.error('❌ Skip button not found in DOM!');
|
||||
}
|
||||
|
||||
console.log('✅ Update downloaded, ready to install. autoInstallSupported:', autoInstallSupported);
|
||||
console.log('✅ Update downloaded, ready to install');
|
||||
}
|
||||
|
||||
handleUpdateError(errorInfo) {
|
||||
@@ -366,9 +368,6 @@ class ClientUpdateManager {
|
||||
|
||||
if (errorMessage && errorText) {
|
||||
let message = errorInfo.message || 'An error occurred during the update process.';
|
||||
if (errorInfo.isMacSigningError) {
|
||||
message = 'Auto-update requires code signing. Please download manually.';
|
||||
}
|
||||
errorText.textContent = message;
|
||||
errorMessage.style.display = 'block';
|
||||
}
|
||||
|
||||
1687
GUI/style.css
1687
GUI/style.css
File diff suppressed because it is too large
Load Diff
@@ -529,7 +529,7 @@ function getAllUuidMappingsArray() {
|
||||
* Validates UUID format before saving
|
||||
* Preserves original case of username
|
||||
*/
|
||||
function setUuidForUser(username, uuid) {
|
||||
function setUuidForUser(username, uuid, { force = false } = {}) {
|
||||
const { validate: validateUuid } = require('uuid');
|
||||
|
||||
if (!username || typeof username !== 'string' || !username.trim()) {
|
||||
@@ -543,15 +543,29 @@ function setUuidForUser(username, uuid) {
|
||||
const displayName = username.trim();
|
||||
const normalizedLookup = displayName.toLowerCase();
|
||||
|
||||
// 1. Update UUID store (source of truth)
|
||||
// 1. Check for existing entries — reject overwrite unless forced
|
||||
migrateUuidStoreIfNeeded();
|
||||
const uuidStore = loadUuidStore();
|
||||
const storeKey = Object.keys(uuidStore).find(k => k.toLowerCase() === normalizedLookup);
|
||||
if (storeKey && uuidStore[storeKey] !== uuid && !force) {
|
||||
console.log(`[Config] Rejected UUID overwrite for "${displayName}": existing ${uuidStore[storeKey]}, attempted ${uuid}`);
|
||||
return { success: false, error: 'duplicate', existingUuid: uuidStore[storeKey] };
|
||||
}
|
||||
// Check if UUID already used by a different name
|
||||
if (!force) {
|
||||
const existingByUuid = Object.entries(uuidStore).find(([k, v]) => v.toLowerCase() === uuid.toLowerCase() && k.toLowerCase() !== normalizedLookup);
|
||||
if (existingByUuid) {
|
||||
console.log(`[Config] Rejected duplicate UUID for "${displayName}": UUID ${uuid} already used by "${existingByUuid[0]}"`);
|
||||
return { success: false, error: 'uuid_in_use', existingUsername: existingByUuid[0] };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Update UUID store (source of truth)
|
||||
if (storeKey) delete uuidStore[storeKey];
|
||||
uuidStore[displayName] = uuid;
|
||||
saveUuidStore(uuidStore);
|
||||
|
||||
// 2. Update config.json (backward compat)
|
||||
// 3. Update config.json (backward compat)
|
||||
const config = loadConfig();
|
||||
const userUuids = config.userUuids || {};
|
||||
const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
||||
@@ -560,7 +574,7 @@ function setUuidForUser(username, uuid) {
|
||||
saveConfig({ userUuids });
|
||||
|
||||
console.log(`[Config] UUID set for "${displayName}": ${uuid}`);
|
||||
return uuid;
|
||||
return { success: true, uuid };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -619,7 +633,7 @@ function resetCurrentUserUuid() {
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const newUuid = uuidv4();
|
||||
|
||||
return setUuidForUser(username, newUuid);
|
||||
return setUuidForUser(username, newUuid, { force: true });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -1073,6 +1087,48 @@ function _generateWindowsWrapper(stripFlags, alwaysArgs, serverArgs) {
|
||||
return lines.join('\r\n');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MATCHA SOCIAL AUTH
|
||||
// =============================================================================
|
||||
|
||||
function saveMatchaToken(token) {
|
||||
saveConfig({ matchaToken: token || null });
|
||||
}
|
||||
|
||||
function loadMatchaToken() {
|
||||
const config = loadConfig();
|
||||
return config.matchaToken || null;
|
||||
}
|
||||
|
||||
function saveMatchaHandle(handle) {
|
||||
saveConfig({ matchaHandle: handle || null });
|
||||
}
|
||||
|
||||
function loadMatchaHandle() {
|
||||
const config = loadConfig();
|
||||
return config.matchaHandle || null;
|
||||
}
|
||||
|
||||
function saveMatchaUserId(id) {
|
||||
saveConfig({ matchaUserId: id || null });
|
||||
}
|
||||
|
||||
function loadMatchaUserId() {
|
||||
const config = loadConfig();
|
||||
return config.matchaUserId || null;
|
||||
}
|
||||
|
||||
function clearMatchaAuth() {
|
||||
const config = loadConfig();
|
||||
delete config.matchaToken;
|
||||
delete config.matchaUserId;
|
||||
delete config.matchaHandle;
|
||||
const data = JSON.stringify(config, null, 2);
|
||||
fs.writeFileSync(CONFIG_TEMP, data, 'utf8');
|
||||
if (fs.existsSync(CONFIG_FILE)) fs.copyFileSync(CONFIG_FILE, CONFIG_BACKUP);
|
||||
fs.renameSync(CONFIG_TEMP, CONFIG_FILE);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
@@ -1148,6 +1204,15 @@ module.exports = {
|
||||
resetWrapperConfig,
|
||||
generateWrapperScript,
|
||||
|
||||
// Matcha Social
|
||||
saveMatchaToken,
|
||||
loadMatchaToken,
|
||||
saveMatchaHandle,
|
||||
loadMatchaHandle,
|
||||
saveMatchaUserId,
|
||||
loadMatchaUserId,
|
||||
clearMatchaAuth,
|
||||
|
||||
// Constants
|
||||
CONFIG_FILE,
|
||||
UUID_STORE_FILE
|
||||
|
||||
@@ -43,24 +43,51 @@ try {
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Fetch tokens from the auth server (properly signed with server's Ed25519 key)
|
||||
async function fetchAuthTokens(uuid, name) {
|
||||
async function fetchAuthTokens(uuid, name, password) {
|
||||
const authServerUrl = getAuthServerUrl();
|
||||
try {
|
||||
console.log(`Fetching auth tokens from ${authServerUrl}/game-session/child`);
|
||||
|
||||
const bodyData = {
|
||||
uuid: uuid,
|
||||
name: name,
|
||||
scopes: ['hytale:server', 'hytale:client']
|
||||
};
|
||||
if (password) bodyData.password = password;
|
||||
|
||||
const response = await fetch(`${authServerUrl}/game-session/child`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
uuid: uuid,
|
||||
name: name,
|
||||
scopes: ['hytale:server', 'hytale:client']
|
||||
})
|
||||
body: JSON.stringify(bodyData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errBody = await response.json().catch(() => ({}));
|
||||
if (response.status === 401 && errBody.password_required) {
|
||||
const err = new Error('Password required');
|
||||
err.passwordRequired = true;
|
||||
err.attemptsRemaining = errBody.attemptsRemaining;
|
||||
throw err;
|
||||
}
|
||||
if (response.status === 429) {
|
||||
const err = new Error('Too many failed attempts. Try again later.');
|
||||
err.lockedOut = true;
|
||||
err.lockoutSeconds = errBody.lockoutSeconds;
|
||||
throw err;
|
||||
}
|
||||
if (response.status === 403 && errBody.username_taken) {
|
||||
const err = new Error('This username is reserved by another player who has set a password. Please use a different name.');
|
||||
err.usernameTaken = true;
|
||||
throw err;
|
||||
}
|
||||
if (response.status === 403 && errBody.name_locked) {
|
||||
const err = new Error(`This UUID is locked to username "${errBody.registeredName}". Change your identity name to match.`);
|
||||
err.nameLocked = true;
|
||||
err.registeredName = errBody.registeredName;
|
||||
throw err;
|
||||
}
|
||||
throw new Error(`Auth server returned ${response.status}`);
|
||||
}
|
||||
|
||||
@@ -77,10 +104,12 @@ async function fetchAuthTokens(uuid, name) {
|
||||
if (payload.username && payload.username !== name && name !== 'Player') {
|
||||
console.warn(`[Auth] Token username mismatch: token has "${payload.username}", expected "${name}". Retrying...`);
|
||||
// Retry once with explicit name
|
||||
const retryBody = { uuid: uuid, name: name, scopes: ['hytale:server', 'hytale:client'] };
|
||||
if (password) retryBody.password = password;
|
||||
const retryResponse = await fetch(`${authServerUrl}/game-session/child`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ uuid: uuid, name: name, scopes: ['hytale:server', 'hytale:client'] })
|
||||
body: JSON.stringify(retryBody)
|
||||
});
|
||||
if (retryResponse.ok) {
|
||||
const retryData = await retryResponse.json();
|
||||
@@ -99,6 +128,10 @@ async function fetchAuthTokens(uuid, name) {
|
||||
console.log('Auth tokens received from server');
|
||||
return { identityToken, sessionToken };
|
||||
} catch (error) {
|
||||
// Re-throw authentication errors — must not fall back to local tokens
|
||||
if (error.passwordRequired || error.lockedOut || error.usernameTaken || error.nameLocked) {
|
||||
throw error;
|
||||
}
|
||||
console.error('Failed to fetch auth tokens:', error.message);
|
||||
// Fallback to local generation if server unavailable
|
||||
return generateLocalTokens(uuid, name);
|
||||
@@ -147,7 +180,7 @@ function generateLocalTokens(uuid, name) {
|
||||
};
|
||||
}
|
||||
|
||||
async function launchGame(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||
async function launchGame(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null, options = {}) {
|
||||
// ==========================================================================
|
||||
// CACHE INVALIDATION: Clear proxyClient module cache to force fresh .env load
|
||||
// This prevents stale cached values from affecting multiple launch attempts
|
||||
@@ -256,11 +289,12 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
|
||||
const uuid = getUuidForUser(playerName);
|
||||
console.log(`[Launcher] UUID for "${playerName}": ${uuid} (verify this stays constant across launches)`);
|
||||
|
||||
// Fetch tokens from auth server
|
||||
// Fetch tokens from auth server (with password if provided)
|
||||
if (progressCallback) {
|
||||
progressCallback('Fetching authentication tokens...', null, null, null, null);
|
||||
}
|
||||
const { identityToken, sessionToken } = await fetchAuthTokens(uuid, playerName);
|
||||
const launchPassword = options?.password || null;
|
||||
const { identityToken, sessionToken } = await fetchAuthTokens(uuid, playerName, launchPassword);
|
||||
|
||||
// Patch client and server binaries to use custom auth server (BEFORE signing on macOS)
|
||||
// FORCE patch on every launch to ensure consistency
|
||||
@@ -411,6 +445,17 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
|
||||
|
||||
const env = { ...process.env };
|
||||
|
||||
// Linux: Add Client directory to LD_LIBRARY_PATH so the dynamic linker can find
|
||||
// bundled native libraries (e.g. libSDL3_image.so.0). The .NET DllImport only tries
|
||||
// bare names like "SDL3_image.so" which don't match versioned .so.0 files.
|
||||
// LD_LIBRARY_PATH lets dlopen() find them via standard library resolution.
|
||||
if (process.platform === 'linux') {
|
||||
const clientDir = path.dirname(clientPath);
|
||||
const existing = env.LD_LIBRARY_PATH || '';
|
||||
env.LD_LIBRARY_PATH = existing ? `${clientDir}:${existing}` : clientDir;
|
||||
console.log(`Linux: LD_LIBRARY_PATH includes ${clientDir}`);
|
||||
}
|
||||
|
||||
const waylandEnv = setupWaylandEnvironment();
|
||||
Object.assign(env, waylandEnv);
|
||||
const gpuEnv = setupGpuEnvironment(gpuPreference);
|
||||
@@ -488,8 +533,8 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
|
||||
if (fs.existsSync(agentJar)) {
|
||||
const agentFlag = `-javaagent:"${agentJar}"`;
|
||||
env.JAVA_TOOL_OPTIONS = env.JAVA_TOOL_OPTIONS
|
||||
? `${env.JAVA_TOOL_OPTIONS} ${agentFlag} -Xshare:off`
|
||||
: `${agentFlag} -Xshare:off`;
|
||||
? `${env.JAVA_TOOL_OPTIONS} ${agentFlag}`
|
||||
: agentFlag;
|
||||
console.log('DualAuth Agent: enabled via JAVA_TOOL_OPTIONS');
|
||||
}
|
||||
|
||||
@@ -578,7 +623,7 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
|
||||
}
|
||||
}
|
||||
|
||||
async function launchGameWithVersionCheck(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||
async function launchGameWithVersionCheck(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null, options = {}) {
|
||||
try {
|
||||
// ==========================================================================
|
||||
// PRE-LAUNCH VALIDATION: Check username is configured
|
||||
@@ -651,7 +696,7 @@ async function launchGameWithVersionCheck(playerNameOverride = null, progressCal
|
||||
progressCallback('Launching game...', 80, null, null, null);
|
||||
}
|
||||
|
||||
const launchResult = await launchGame(playerNameOverride, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
|
||||
const launchResult = await launchGame(playerNameOverride, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch, options);
|
||||
|
||||
// Ensure we always return a result
|
||||
if (!launchResult) {
|
||||
@@ -665,6 +710,10 @@ async function launchGameWithVersionCheck(playerNameOverride = null, progressCal
|
||||
if (progressCallback) {
|
||||
progressCallback(`Error: ${error.message}`, -1, null, null, null);
|
||||
}
|
||||
// Re-throw authentication errors so IPC handler can return proper flags
|
||||
if (error.passwordRequired || error.lockedOut || error.usernameTaken || error.nameLocked) {
|
||||
throw error;
|
||||
}
|
||||
// Always return an error response instead of throwing
|
||||
return { success: false, error: error.message || 'Unknown launch error' };
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ const { v4: uuidv4 } = require('uuid');
|
||||
const {
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
getModsPath
|
||||
getModsPath,
|
||||
loadVersionBranch,
|
||||
saveVersionBranch,
|
||||
loadVersionClient,
|
||||
saveVersionClient
|
||||
} = require('../core/config');
|
||||
|
||||
// Lazy-load modManager to avoid circular deps, or keep imports structured.
|
||||
@@ -44,6 +48,8 @@ class ProfileManager {
|
||||
// If global settings existed, we copy them here
|
||||
mods: config.installedMods || [], // Legacy mods are now part of default profile
|
||||
javaPath: config.javaPath || '',
|
||||
versionBranch: config.version_branch || 'release',
|
||||
versionClient: config.version_client || null,
|
||||
gameOptions: {
|
||||
minMemory: '1G',
|
||||
maxMemory: '4G',
|
||||
@@ -73,6 +79,7 @@ class ProfileManager {
|
||||
const config = loadConfig();
|
||||
const id = uuidv4();
|
||||
|
||||
// New profiles inherit the current branch/version
|
||||
const newProfile = {
|
||||
id,
|
||||
name: name.trim(),
|
||||
@@ -80,6 +87,8 @@ class ProfileManager {
|
||||
lastUsed: null,
|
||||
mods: [], // Start with no mods enabled
|
||||
javaPath: '',
|
||||
versionBranch: loadVersionBranch(),
|
||||
versionClient: loadVersionClient(),
|
||||
gameOptions: {
|
||||
minMemory: '1G',
|
||||
maxMemory: '4G',
|
||||
@@ -129,19 +138,34 @@ class ProfileManager {
|
||||
|
||||
console.log(`[ProfileManager] Switching to profile: ${config.profiles[id].name} (${id})`);
|
||||
|
||||
// 1. Update config first
|
||||
// Save current branch/version to the outgoing profile
|
||||
const oldId = config.activeProfileId;
|
||||
if (oldId && config.profiles[oldId]) {
|
||||
config.profiles[oldId].versionBranch = loadVersionBranch();
|
||||
config.profiles[oldId].versionClient = loadVersionClient();
|
||||
}
|
||||
|
||||
// 1. Update config
|
||||
config.profiles[id].lastUsed = new Date().toISOString();
|
||||
saveConfig({
|
||||
activeProfileId: id,
|
||||
profiles: config.profiles
|
||||
});
|
||||
|
||||
// 2. Trigger Mod Sync
|
||||
// We need to require this here to ensure it uses the *newly saved* active profile ID
|
||||
// 2. Restore branch/version from the new profile
|
||||
const newProfile = config.profiles[id];
|
||||
if (newProfile.versionBranch) {
|
||||
saveVersionBranch(newProfile.versionBranch);
|
||||
}
|
||||
if (newProfile.versionClient !== undefined) {
|
||||
saveVersionClient(newProfile.versionClient);
|
||||
}
|
||||
|
||||
// 3. Trigger Mod Sync
|
||||
const { syncModsForCurrentProfile } = require('./modManager');
|
||||
await syncModsForCurrentProfile();
|
||||
|
||||
return config.profiles[id];
|
||||
return newProfile;
|
||||
}
|
||||
|
||||
deleteProfile(id) {
|
||||
@@ -177,7 +201,7 @@ class ProfileManager {
|
||||
}
|
||||
|
||||
// Safety checks on updates
|
||||
const allowedFields = ['name', 'javaPath', 'gameOptions', 'mods'];
|
||||
const allowedFields = ['name', 'javaPath', 'gameOptions', 'mods', 'versionBranch', 'versionClient'];
|
||||
const sanitizedUpdates = {};
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
|
||||
529
backend/services/matchaService.js
Normal file
529
backend/services/matchaService.js
Normal file
@@ -0,0 +1,529 @@
|
||||
const axios = require('axios');
|
||||
const WebSocket = require('ws');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { loadConfig, saveConfig } = require('../core/config');
|
||||
|
||||
const MATCHA_BASE = 'https://butter.lat';
|
||||
const MATCHA_API = `${MATCHA_BASE}/api/matcha`;
|
||||
const MATCHA_WS = 'wss://butter.lat/api/matcha/ws';
|
||||
|
||||
class MatchaService {
|
||||
constructor() {
|
||||
this.token = null;
|
||||
this.user = null;
|
||||
this.ws = null;
|
||||
this.wsConnected = false;
|
||||
this.mainWindow = null;
|
||||
this.heartbeatInterval = null;
|
||||
this.reconnectTimeout = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
this.lastMessageSentAt = 0;
|
||||
this.gameRunning = false;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// LIFECYCLE
|
||||
// =========================================================================
|
||||
|
||||
init(mainWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
this.token = this._loadToken();
|
||||
if (this.token) {
|
||||
this._connectWs();
|
||||
this._startHeartbeat();
|
||||
// Load cached user info
|
||||
const config = loadConfig();
|
||||
if (config.matchaUserId && config.matchaHandle) {
|
||||
this.user = { id: config.matchaUserId, handle: config.matchaHandle };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Best-effort offline heartbeat
|
||||
if (this.token) {
|
||||
this.sendHeartbeat('offline').catch(() => {});
|
||||
}
|
||||
this._stopHeartbeat();
|
||||
this._disconnectWs();
|
||||
this.mainWindow = null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TOKEN MANAGEMENT
|
||||
// =========================================================================
|
||||
|
||||
_loadToken() {
|
||||
const config = loadConfig();
|
||||
return config.matchaToken || null;
|
||||
}
|
||||
|
||||
_saveToken(token) {
|
||||
this.token = token;
|
||||
saveConfig({ matchaToken: token });
|
||||
}
|
||||
|
||||
_saveUser(user) {
|
||||
this.user = user;
|
||||
if (user) {
|
||||
saveConfig({ matchaUserId: user.id, matchaHandle: user.handle });
|
||||
}
|
||||
}
|
||||
|
||||
_clearAuth() {
|
||||
this.token = null;
|
||||
this.user = null;
|
||||
this._stopHeartbeat();
|
||||
this._disconnectWs();
|
||||
// Set to null — saveConfig merges, and JSON.stringify preserves null values,
|
||||
// but this effectively marks them as cleared for _loadToken() checks
|
||||
saveConfig({ matchaToken: null, matchaUserId: null, matchaHandle: null });
|
||||
}
|
||||
|
||||
_authHeaders() {
|
||||
return this.token ? { Authorization: `Bearer ${this.token}` } : {};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AUTH
|
||||
// =========================================================================
|
||||
|
||||
async register(username, password, password2) {
|
||||
try {
|
||||
const res = await axios.post(`${MATCHA_API}/register`, {
|
||||
username, password, password2, deferCreate: true
|
||||
});
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async confirmRegistration(pendingId, proofId) {
|
||||
try {
|
||||
const res = await axios.post(`${MATCHA_API}/register/confirm`, {
|
||||
pendingId, proofId
|
||||
});
|
||||
if (res.data.token) {
|
||||
this._saveToken(res.data.token);
|
||||
if (res.data.user) this._saveUser(res.data.user);
|
||||
this._connectWs();
|
||||
this._startHeartbeat();
|
||||
}
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async login(handle, password) {
|
||||
try {
|
||||
const res = await axios.post(`${MATCHA_API}/login`, { handle, password });
|
||||
if (res.data.token) {
|
||||
this._saveToken(res.data.token);
|
||||
if (res.data.user) this._saveUser(res.data.user);
|
||||
this._connectWs();
|
||||
this._startHeartbeat();
|
||||
}
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await this.sendHeartbeat('offline').catch(() => {});
|
||||
this._clearAuth();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
getAuthState() {
|
||||
return {
|
||||
authenticated: !!this.token,
|
||||
user: this.user,
|
||||
wsConnected: this.wsConnected
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// PROFILE
|
||||
// =========================================================================
|
||||
|
||||
async getMe() {
|
||||
try {
|
||||
const res = await axios.get(`${MATCHA_API}/me`, { headers: this._authHeaders() });
|
||||
if (res.data?.user) this._saveUser({ id: res.data.user.id, handle: res.data.user.handle });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getUser(userId) {
|
||||
try {
|
||||
const res = await axios.get(`${MATCHA_API}/users/${encodeURIComponent(userId)}`, { headers: this._authHeaders() });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// FRIENDS
|
||||
// =========================================================================
|
||||
|
||||
async getFriends() {
|
||||
try {
|
||||
const res = await axios.get(`${MATCHA_API}/friends`, { headers: this._authHeaders() });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async sendFriendRequest(handle) {
|
||||
try {
|
||||
const res = await axios.post(`${MATCHA_API}/friends/request`, { toHandle: handle }, { headers: this._authHeaders() });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async acceptFriend(requestId) {
|
||||
try {
|
||||
const res = await axios.post(`${MATCHA_API}/friends/request/accept`, { id: requestId }, { headers: this._authHeaders() });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async rejectFriend(requestId) {
|
||||
try {
|
||||
const res = await axios.post(`${MATCHA_API}/friends/request/reject`, { id: requestId }, { headers: this._authHeaders() });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async cancelFriendRequest(requestId) {
|
||||
try {
|
||||
const res = await axios.post(`${MATCHA_API}/friends/request/cancel`, { id: requestId }, { headers: this._authHeaders() });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async removeFriend(friendId) {
|
||||
try {
|
||||
const res = await axios.post(`${MATCHA_API}/friends/remove`, { friendId }, { headers: this._authHeaders() });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// MESSAGES
|
||||
// =========================================================================
|
||||
|
||||
async getMessages(withTarget, cursor, after) {
|
||||
try {
|
||||
const params = { with: withTarget };
|
||||
if (cursor) params.cursor = cursor;
|
||||
if (after) params.after = after;
|
||||
const res = await axios.get(`${MATCHA_API}/messages`, { headers: this._authHeaders(), params });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage(to, body, replyTo) {
|
||||
// Enforce 800ms throttle
|
||||
const now = Date.now();
|
||||
const elapsed = now - this.lastMessageSentAt;
|
||||
if (elapsed < 800) {
|
||||
return { ok: false, error: 'Please wait before sending another message' };
|
||||
}
|
||||
this.lastMessageSentAt = now;
|
||||
|
||||
// Try WebSocket first
|
||||
if (this.wsConnected && this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
const msg = { type: 'send', to, body };
|
||||
if (replyTo) msg.replyTo = replyTo;
|
||||
this.ws.send(JSON.stringify(msg));
|
||||
return { ok: true, data: { sent: true, via: 'ws' } };
|
||||
}
|
||||
|
||||
// Fallback to HTTP
|
||||
try {
|
||||
const payload = { to, body };
|
||||
if (replyTo) payload.replyTo = replyTo;
|
||||
const res = await axios.post(`${MATCHA_API}/messages/send`, payload, { headers: this._authHeaders() });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMessage(messageId) {
|
||||
try {
|
||||
const res = await axios.post(`${MATCHA_API}/messages/${messageId}/delete`, {}, { headers: this._authHeaders() });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// UNREAD
|
||||
// =========================================================================
|
||||
|
||||
async getUnread() {
|
||||
try {
|
||||
const res = await axios.get(`${MATCHA_API}/unread`, { headers: this._authHeaders() });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async clearUnread(withTarget) {
|
||||
try {
|
||||
const res = await axios.post(`${MATCHA_API}/unread/clear`, { with: withTarget }, { headers: this._authHeaders() });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// HEARTBEAT
|
||||
// =========================================================================
|
||||
|
||||
async sendHeartbeat(state) {
|
||||
if (!this.token) return;
|
||||
try {
|
||||
await axios.post(`${MATCHA_API}/heartbeat`, { state: state || 'online' }, { headers: this._authHeaders() });
|
||||
} catch (err) {
|
||||
console.log('[Matcha] Heartbeat failed:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
_startHeartbeat() {
|
||||
this._stopHeartbeat();
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
const state = this.gameRunning ? 'in_game' : 'online';
|
||||
this.sendHeartbeat(state);
|
||||
}, 30000);
|
||||
// Send initial heartbeat
|
||||
this.sendHeartbeat('online');
|
||||
}
|
||||
|
||||
_stopHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
setGameRunning(running) {
|
||||
this.gameRunning = running;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// AVATAR
|
||||
// =========================================================================
|
||||
|
||||
async uploadAvatar(filePath, mode) {
|
||||
try {
|
||||
const crypto = require('crypto');
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
if (fileBuffer.length > 1024 * 1024) {
|
||||
return { ok: false, error: 'Avatar too large (max 1MB)' };
|
||||
}
|
||||
const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
||||
const endpoint = mode === 'custom' ? `${MATCHA_API}/avatar/custom` : `${MATCHA_API}/avatar`;
|
||||
const res = await axios.post(endpoint, fileBuffer, {
|
||||
headers: {
|
||||
...this._authHeaders(),
|
||||
'Content-Type': 'image/png',
|
||||
'x-avatar-hash': hash,
|
||||
'x-avatar-enable': '1',
|
||||
'x-avatar-force': '1',
|
||||
'Cache-Control': 'no-store'
|
||||
},
|
||||
maxContentLength: 1024 * 1024
|
||||
});
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAvatar() {
|
||||
try {
|
||||
const res = await axios.delete(`${MATCHA_API}/avatar`, { headers: this._authHeaders() });
|
||||
return { ok: true, data: res.data };
|
||||
} catch (err) {
|
||||
return this._handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// WEBSOCKET
|
||||
// =========================================================================
|
||||
|
||||
_connectWs() {
|
||||
if (this.ws) this._disconnectWs();
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(MATCHA_WS);
|
||||
|
||||
this.ws.on('open', () => {
|
||||
console.log('[Matcha] WebSocket connected');
|
||||
this.reconnectAttempts = 0;
|
||||
// Authenticate
|
||||
if (this.token) {
|
||||
this.ws.send(JSON.stringify({ type: 'auth', token: this.token }));
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('message', (raw) => {
|
||||
try {
|
||||
const data = JSON.parse(raw.toString());
|
||||
this._handleWsMessage(data);
|
||||
} catch (err) {
|
||||
console.error('[Matcha] WS parse error:', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('close', (code) => {
|
||||
console.log('[Matcha] WebSocket closed:', code);
|
||||
this.wsConnected = false;
|
||||
this._sendToRenderer('matcha:ws:disconnected');
|
||||
|
||||
// Handle ban
|
||||
if (code === 4003) {
|
||||
this._sendToRenderer('matcha:ws:banned', { reason: 'Account banned' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-reconnect if we have a token
|
||||
if (this.token && code !== 4003) {
|
||||
this._scheduleReconnect();
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('error', (err) => {
|
||||
console.error('[Matcha] WS error:', err.message);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Matcha] WS connect failed:', err.message);
|
||||
this._scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
_disconnectWs() {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = null;
|
||||
}
|
||||
if (this.ws) {
|
||||
try {
|
||||
this.ws.close();
|
||||
} catch (e) {}
|
||||
this.ws = null;
|
||||
}
|
||||
this.wsConnected = false;
|
||||
}
|
||||
|
||||
_scheduleReconnect() {
|
||||
if (this.reconnectTimeout) return;
|
||||
// Exponential backoff capped at 30s, no hard limit (matches Butter's infinite reconnect)
|
||||
const delay = Math.min(2000 * Math.pow(2, this.reconnectAttempts), 30000);
|
||||
this.reconnectAttempts++;
|
||||
// Notify renderer after several failures so it can show a banner
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
this._sendToRenderer('matcha:ws:max-retries');
|
||||
}
|
||||
console.log(`[Matcha] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
||||
this.reconnectTimeout = setTimeout(() => {
|
||||
this.reconnectTimeout = null;
|
||||
if (this.token) this._connectWs();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
_handleWsMessage(data) {
|
||||
switch (data.type) {
|
||||
case 'authed':
|
||||
this.wsConnected = true;
|
||||
if (data.user) this._saveUser(data.user);
|
||||
this._sendToRenderer('matcha:ws:connected', { user: this.user });
|
||||
break;
|
||||
case 'message':
|
||||
console.log('[Matcha] WS message received:', JSON.stringify(data).substring(0, 200));
|
||||
this._sendToRenderer('matcha:ws:message', data);
|
||||
break;
|
||||
case 'message_deleted':
|
||||
this._sendToRenderer('matcha:ws:message-deleted', data);
|
||||
break;
|
||||
case 'avatar_updated':
|
||||
this._sendToRenderer('matcha:ws:avatar-updated', data);
|
||||
break;
|
||||
case 'banned':
|
||||
this._sendToRenderer('matcha:ws:banned', data);
|
||||
break;
|
||||
case 'announcement':
|
||||
this._sendToRenderer('matcha:ws:announcement', data);
|
||||
break;
|
||||
case 'error':
|
||||
console.log('[Matcha] WS error:', data.message || data.error || JSON.stringify(data));
|
||||
// If auth error, treat as ban/disconnect
|
||||
if (data.message === 'Not authed' || data.error === 'Not authed') {
|
||||
this._clearAuth();
|
||||
this._sendToRenderer('matcha:ws:disconnected');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log('[Matcha] Unknown WS message type:', data.type);
|
||||
}
|
||||
}
|
||||
|
||||
manualReconnect() {
|
||||
this.reconnectAttempts = 0;
|
||||
if (this.token) this._connectWs();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// HELPERS
|
||||
// =========================================================================
|
||||
|
||||
_sendToRenderer(channel, data) {
|
||||
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
|
||||
this.mainWindow.webContents.send(channel, data);
|
||||
}
|
||||
}
|
||||
|
||||
_handleError(err) {
|
||||
if (err.response) {
|
||||
const status = err.response.status;
|
||||
const msg = err.response.data?.error || err.response.data?.message || err.message;
|
||||
if (status === 401) {
|
||||
// Token expired or invalid
|
||||
this._clearAuth();
|
||||
this._sendToRenderer('matcha:ws:disconnected');
|
||||
}
|
||||
return { ok: false, error: msg, status };
|
||||
}
|
||||
return { ok: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new MatchaService();
|
||||
@@ -594,6 +594,10 @@ function setupGpuEnvironment(gpuPreference) {
|
||||
if (detected.vendor === 'nvidia') {
|
||||
envVars.__NV_PRIME_RENDER_OFFLOAD = '1';
|
||||
envVars.__GLX_VENDOR_LIBRARY_NAME = 'nvidia';
|
||||
// Prevent Wayland explicit sync crashes on NVIDIA (Hyprland, etc.)
|
||||
if (isWaylandSession()) {
|
||||
envVars.__NV_DISABLE_EXPLICIT_SYNC = '1';
|
||||
}
|
||||
const nvidiaEglFile = '/usr/share/glvnd/egl_vendor.d/10_nvidia.json';
|
||||
if (fs.existsSync(nvidiaEglFile)) {
|
||||
envVars.__EGL_VENDOR_LIBRARY_FILENAMES = nvidiaEglFile;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Singleplayer Server Crash: fastutil ClassNotFoundException
|
||||
|
||||
## Status: Open (multiple users, Feb 24-27 2026)
|
||||
## Status: Open — likely outdated HytaleServer.jar (Feb 24-28 2026)
|
||||
|
||||
## Symptom
|
||||
|
||||
Singleplayer server crashes immediately after DualAuth Agent installs successfully:
|
||||
Singleplayer server crashes immediately on boot:
|
||||
|
||||
```
|
||||
Exception in thread "main" java.lang.NoClassDefFoundError: it/unimi/dsi/fastutil/objects/ObjectArrayList
|
||||
@@ -20,104 +20,71 @@ Server exits with code 1. Multiplayer works fine for the same user.
|
||||
|
||||
1. **ヅ𝚃 JAYED !** (Feb 24) — Windows x86_64, had AOT cache errors before fastutil crash
|
||||
2. **Asentrix** (Feb 27) — Windows x86_64 (NT 10.0.26200.0), RTX 4060, Launcher v2.4.4, NO AOT cache errors
|
||||
3. **7645754** (Feb 28) — Standalone server on localhost, **FIXED by updating HytaleServer.jar**
|
||||
|
||||
- Reproduces 100% on singleplayer, every attempt
|
||||
- Multiplayer works fine for both users
|
||||
- Reproduces 100% on singleplayer, every attempt (users 1-2)
|
||||
- Multiplayer works fine for users 1-2
|
||||
- macOS/Linux users are NOT affected
|
||||
|
||||
## What Works
|
||||
## Ruled Out (confirmed via debug builds)
|
||||
|
||||
- Java wrapper correctly strips `-XX:+UseCompactObjectHeaders`
|
||||
- Java wrapper correctly injects `--disable-sentry`
|
||||
- DualAuth Agent v1.1.12 installs successfully (STATIC mode)
|
||||
- Multiplayer connections work fine
|
||||
- Repair and reinstall did NOT fix the issue
|
||||
| Suspect | Tested | Result |
|
||||
|---------|--------|--------|
|
||||
| **DualAuth Agent** | Debug build with agent completely disabled (`debug-no-agent` tag) | **Same crash.** Agent is innocent. |
|
||||
| **`-Xshare:off` (CDS)** | Added to `JAVA_TOOL_OPTIONS` in launcher code (`debug-xshare-off` tag) | **Did not help.** CDS is not the cause. |
|
||||
| **`-XX:+UseCompactObjectHeaders`** | Stripped via wrapper | **Did not help.** Server has `-XX:+IgnoreUnrecognizedVMOptions` anyway. |
|
||||
| **Corrupted game files** | User did repair + full reinstall | **Same crash.** |
|
||||
| **Java wrapper** | Logs confirm wrapper works correctly | Not the cause. |
|
||||
| **ARM64/Parallels** | User is on standard Windows x86_64 | Not applicable. |
|
||||
| **AOT cache** | Asentrix has no AOT errors (JAYED did), both crash the same way | Not the root cause. |
|
||||
|
||||
## Root Cause Analysis
|
||||
## Key Finding: Outdated HytaleServer.jar (Feb 28)
|
||||
|
||||
`fastutil` (`it.unimi.dsi.fastutil`) should be bundled inside `HytaleServer.jar` (fat JAR). The `ClassNotFoundException` means the JVM's app classloader cannot find it despite it being in the JAR.
|
||||
User `7645754` had the **exact same error** on their standalone localhost server but NOT on their VPS. **Fixed by replacing `HytaleServer.jar` with the current version.** The old JAR used to work but stopped — likely the bundled JRE was updated and is now incompatible with older JAR versions.
|
||||
|
||||
### Ruled Out
|
||||
This strongly suggests the root cause for F2P launcher users is also a **stale/mismatched `HytaleServer.jar`**. The launcher may report the correct version but the actual file on disk could be from an older download.
|
||||
|
||||
- **Wrapper issue**: Wrapper is working correctly (confirmed in logs)
|
||||
- **UseCompactObjectHeaders**: Server also has `-XX:+IgnoreUnrecognizedVMOptions`, so unrecognized flags don't crash it
|
||||
- **DualAuth Agent**: Works for all other users; agent installs successfully before the crash
|
||||
- **Corrupted game files**: Repair/reinstall didn't help
|
||||
- **ARM64/Parallels**: User is on standard Windows, not ARM
|
||||
## What We Know
|
||||
|
||||
### Likely Cause
|
||||
- `fastutil` is bundled inside `HytaleServer.jar` (fat/shaded JAR)
|
||||
- JVM's `BuiltinClassLoader` cannot find `it.unimi.dsi.fastutil.objects.ObjectArrayList` despite it being in the JAR
|
||||
- Crash happens at `EarlyPluginLoader` static initializer (line 34) which imports `ObjectArrayList`
|
||||
- **Replacing `HytaleServer.jar` with a fresh copy fixes the issue** (confirmed by user 3)
|
||||
- The issue is NOT caused by the DualAuth agent or any launcher modification
|
||||
|
||||
**CDS (Class Data Sharing) broken by bootstrap classloader modification.** DualAuth agent calls `appendToBootstrapClassLoaderSearch()` which triggers JVM warning: `"Sharing is only supported for boot loader classes because bootstrap classpath has been appended"`. This disables AppCDS for application classes. On some Windows systems, this breaks the classloader's ability to find classes (including fastutil) from the fat JAR.
|
||||
## Fix for Users
|
||||
|
||||
This warning appears for ALL users, but only breaks classloading on some Windows systems — reason unknown.
|
||||
### F2P Launcher users (Asentrix, JAYED)
|
||||
1. **Delete the entire game folder**: `%LOCALAPPDATA%\HytaleF2P\release\package\game\`
|
||||
2. Relaunch — launcher will re-download everything fresh
|
||||
3. NOT just "repair" — full delete to ensure no stale files remain
|
||||
|
||||
### Other Possible Causes
|
||||
|
||||
1. **Antivirus interference** — AV blocking Java from reading classes out of JAR files
|
||||
2. **File locking** — another process holding HytaleServer.jar open (Asentrix had stalled java.exe killed at launch)
|
||||
|
||||
## Potential Fix: `-Xshare:off` (testing Feb 27)
|
||||
|
||||
Disables CDS entirely, forcing standard classloading. User can add via launcher:
|
||||
1. **Settings** → **Java Wrapper Configuration** → **Arguments to Inject**
|
||||
2. Add `-Xshare:off` with **Server Only** condition
|
||||
3. Retry singleplayer
|
||||
|
||||
Sent to affected users for testing — **awaiting results**.
|
||||
|
||||
If confirmed, should be added as default inject arg (server-only) in launcher config.
|
||||
|
||||
## Debugging Steps (for reference)
|
||||
|
||||
Most steps are impractical for F2P users:
|
||||
- ~~Official Hytale singleplayer~~ — F2P users don't have official access
|
||||
- ~~Try without DualAuth agent~~ — not possible, agent required for F2P token validation
|
||||
- ~~Verify fastutil in JAR~~ — same JAR for all users, not a user-actionable step
|
||||
- ~~Check JRE version~~ — bundled with launcher, same for all users
|
||||
|
||||
**Practical steps:**
|
||||
1. **Add `-Xshare:off`** via wrapper inject args (server-only) — testing now
|
||||
2. **Check antivirus** — add game directory to Windows Defender exclusions
|
||||
3. **Check for stalled processes** — kill any leftover java.exe/HytaleServer before launch
|
||||
### Standalone server users
|
||||
1. Download fresh `HytaleServer.jar` from current game version
|
||||
2. Replace the old JAR file
|
||||
|
||||
## Update History
|
||||
|
||||
### Feb 24: `-XX:+UseCompactObjectHeaders` stripping removed from defaults
|
||||
Stripping this flag did NOT fix the issue. The server already has `-XX:+IgnoreUnrecognizedVMOptions` so unrecognized flags are harmless. The flag was removed from default `stripFlags` in `backend/core/config.js`.
|
||||
### Feb 24: First report (JAYED)
|
||||
User reported singleplayer crash. Initial investigation found AOT cache errors + fastutil ClassNotFoundException. Stripping `-XX:+UseCompactObjectHeaders` did not help.
|
||||
|
||||
### Feb 27: Second user (Asentrix) reported, `-Xshare:off` sent for testing
|
||||
Asentrix hit the same crash on Launcher v2.4.4. Unlike JAYED, no AOT cache errors — just the CDS sharing warning followed by fastutil ClassNotFoundException. This confirms the issue is not AOT-specific but related to CDS/classloader interaction with the DualAuth agent's bootstrap CL modification. Sent `-Xshare:off` workaround to affected users — awaiting results.
|
||||
### Feb 27: Second report (Asentrix), extensive debugging
|
||||
- Asentrix hit same crash, no AOT errors — ruled out AOT as root cause
|
||||
- Built `debug-xshare-off`: added `-Xshare:off` to `JAVA_TOOL_OPTIONS` — **did not help**
|
||||
- Built `debug-no-agent`: completely disabled DualAuth agent — **same crash**
|
||||
- **Conclusion**: Neither the agent nor CDS is the cause. The JVM itself cannot load classes from the fat JAR on these specific Windows systems.
|
||||
- Note: wrapper `injectArgs` append AFTER `-jar`, so they cannot inject JVM flags — only `JAVA_TOOL_OPTIONS` works for JVM flags
|
||||
|
||||
## Using the Java Wrapper to Strip JVM Flags
|
||||
|
||||
If a user needs to strip a specific JVM flag (e.g., for debugging or compatibility), they can do it via the launcher UI:
|
||||
|
||||
1. Open **Settings** → scroll to **Java Wrapper Configuration**
|
||||
2. Under **JVM Flags to Remove**, type the flag (e.g. `-XX:+UseCompactObjectHeaders`) and click **Add**
|
||||
3. The flag will be stripped from all JVM invocations at launch time
|
||||
4. To inject custom arguments, use the **Arguments to Inject** section (with optional "Server Only" condition)
|
||||
5. **Restore Defaults** resets to empty strip flags + `--disable-sentry` (server only)
|
||||
|
||||
The wrapper generates platform-specific scripts at launch time:
|
||||
- **Windows**: `java-wrapper.bat` in `jre/latest/bin/`
|
||||
- **macOS/Linux**: `java-wrapper` shell script in the same directory
|
||||
|
||||
Config is stored in `config.json` under `javaWrapperConfig`:
|
||||
```json
|
||||
{
|
||||
"javaWrapperConfig": {
|
||||
"stripFlags": ["-XX:+SomeFlag"],
|
||||
"injectArgs": [
|
||||
{ "arg": "--some-arg", "condition": "server" },
|
||||
{ "arg": "--other-arg", "condition": "always" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
### Feb 28: Third user (7645754) — FIXED by replacing HytaleServer.jar
|
||||
- Standalone server user had same crash on localhost, VPS worked fine
|
||||
- **Fixed by updating `HytaleServer.jar` to match VPS version**
|
||||
- Root cause likely: outdated JAR incompatible with current/updated JRE
|
||||
- For F2P launcher users: need to delete game folder and force fresh re-download
|
||||
|
||||
## Related
|
||||
|
||||
- Java wrapper config: `backend/core/config.js` (stripFlags / injectArgs)
|
||||
- DualAuth Agent: v1.1.12, package `ws.sanasol.dualauth`
|
||||
- Game version at time of reports: `2026.02.19-1a311a592`
|
||||
- Log submission ID (Asentrix): `c88e7b71`
|
||||
- Debug tags: `debug-xshare-off`, `debug-no-agent`
|
||||
- Log submission IDs: `c88e7b71` (Asentrix initial), `0445e4dc` (xshare test), `748dceeb` (no-agent test)
|
||||
|
||||
342
main.js
342
main.js
@@ -6,6 +6,7 @@ const fs = require('fs');
|
||||
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, saveAllowMultiInstance, loadAllowMultiInstance, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched, loadConfig, saveConfig, checkLaunchReady } = require('./backend/launcher');
|
||||
const { retryPWRDownload } = require('./backend/managers/gameManager');
|
||||
const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration');
|
||||
const matchaService = require('./backend/services/matchaService');
|
||||
|
||||
// Handle Hardware Acceleration
|
||||
try {
|
||||
@@ -193,7 +194,7 @@ function createWindow() {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
devTools: false,
|
||||
devTools: process.argv.includes('--dev'),
|
||||
webSecurity: true
|
||||
}
|
||||
});
|
||||
@@ -213,6 +214,9 @@ function createWindow() {
|
||||
// Initialize Discord Rich Presence
|
||||
initDiscordRPC();
|
||||
|
||||
// Initialize Matcha Social service
|
||||
matchaService.init(mainWindow);
|
||||
|
||||
// Configure and initialize electron-updater
|
||||
// Enable auto-download so updates start immediately when available
|
||||
autoUpdater.autoDownload = true;
|
||||
@@ -251,7 +255,7 @@ function createWindow() {
|
||||
mainWindow.webContents.send('update-error', {
|
||||
message: err.message,
|
||||
isMacSigningError: isMacSigningError,
|
||||
requiresManualDownload: isMacSigningError || process.platform === 'darwin'
|
||||
requiresManualDownload: isMacSigningError
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -272,9 +276,7 @@ function createWindow() {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('update-downloaded', {
|
||||
version: info.version,
|
||||
platform: process.platform,
|
||||
// macOS auto-install often fails on unsigned apps
|
||||
autoInstallSupported: process.platform !== 'darwin'
|
||||
platform: process.platform
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -286,9 +288,12 @@ function createWindow() {
|
||||
});
|
||||
}, 3000);
|
||||
|
||||
const isDev = process.argv.includes('--dev');
|
||||
if (!isDev) {
|
||||
mainWindow.webContents.on('devtools-opened', () => {
|
||||
mainWindow.webContents.closeDevTools();
|
||||
});
|
||||
}
|
||||
|
||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||
// Allow standard copy/paste/cut/select-all shortcuts
|
||||
@@ -301,7 +306,8 @@ function createWindow() {
|
||||
return; // Don't block these
|
||||
}
|
||||
|
||||
// Block devtools shortcuts
|
||||
// Block devtools shortcuts (except in dev mode)
|
||||
if (!isDev) {
|
||||
if (input.control && input.shift && input.key.toLowerCase() === 'i') {
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -314,6 +320,7 @@ function createWindow() {
|
||||
if (input.key === 'F12') {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
if (input.key === 'F5') {
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -506,6 +513,7 @@ async function cleanupDiscordRPC() {
|
||||
app.on('before-quit', () => {
|
||||
console.log('=== LAUNCHER BEFORE QUIT ===');
|
||||
cleanupDiscordRPC();
|
||||
matchaService.destroy();
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
@@ -530,9 +538,34 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g
|
||||
}
|
||||
};
|
||||
|
||||
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference);
|
||||
// Check if UUID has password before launching
|
||||
let launchOptions = {};
|
||||
const { getAuthServerUrl, getUuidForUser } = require('./backend/core/config');
|
||||
const launchUuid = getUuidForUser(playerName);
|
||||
try {
|
||||
const uuid = launchUuid;
|
||||
const authServerUrl = getAuthServerUrl();
|
||||
const statusResp = await fetch(`${authServerUrl}/player/password/status/${uuid}`);
|
||||
if (statusResp.ok) {
|
||||
const status = await statusResp.json();
|
||||
if (status.hasPassword) {
|
||||
// Return to renderer to prompt for password
|
||||
return { success: false, passwordRequired: true, uuid };
|
||||
}
|
||||
}
|
||||
} catch (pwErr) {
|
||||
console.log('[Launch] Password check skipped:', pwErr.message);
|
||||
}
|
||||
|
||||
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference, null, launchOptions);
|
||||
|
||||
if (result.success && result.launched) {
|
||||
// Save last played timestamp
|
||||
try { saveConfig({ last_played: Date.now() }); } catch (e) { /* ignore */ }
|
||||
|
||||
// Notify Matcha that game is running (heartbeat will send 'in_game')
|
||||
matchaService.setGameRunning(true);
|
||||
|
||||
const closeOnStart = loadCloseLauncherOnStart();
|
||||
if (closeOnStart) {
|
||||
console.log('Close Launcher on start enabled, quitting application...');
|
||||
@@ -554,10 +587,72 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
if (error.passwordRequired) {
|
||||
return { success: false, passwordRequired: true, uuid: launchUuid, error: 'Password required' };
|
||||
}
|
||||
if (error.lockedOut) {
|
||||
return { success: false, error: 'Too many failed attempts. Try again in ' + Math.ceil((error.lockoutSeconds || 900) / 60) + ' minutes.' };
|
||||
}
|
||||
if (error.usernameTaken) {
|
||||
return { success: false, usernameTaken: true, error: errorMessage };
|
||||
}
|
||||
if (error.nameLocked) {
|
||||
return { success: false, nameLocked: true, registeredName: error.registeredName, error: error.message };
|
||||
}
|
||||
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('launch-game-with-password', async (event, playerName, javaPath, installPath, gpuPreference, password) => {
|
||||
try {
|
||||
const progressCallback = (message, percent, speed, downloaded, total, retryState) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('progress-update', {
|
||||
message: message || null,
|
||||
percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null,
|
||||
speed: speed !== null && speed !== undefined ? speed : null,
|
||||
downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null,
|
||||
total: total !== null && total !== undefined ? total : null,
|
||||
retryState: retryState || null
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference, null, { password });
|
||||
|
||||
if (result.success && result.launched) {
|
||||
try { saveConfig({ last_played: Date.now() }); } catch (e) { /* ignore */ }
|
||||
|
||||
matchaService.setGameRunning(true);
|
||||
|
||||
const closeOnStart = loadCloseLauncherOnStart();
|
||||
if (closeOnStart) {
|
||||
setTimeout(() => { app.quit(); }, 1000);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Launch with password error:', error);
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
setTimeout(() => { mainWindow.webContents.send('progress-complete'); }, 2000);
|
||||
}
|
||||
if (error.passwordRequired) {
|
||||
return { success: false, passwordRequired: true, error: 'Incorrect password. ' + (error.attemptsRemaining != null ? error.attemptsRemaining + ' attempts remaining.' : '') };
|
||||
}
|
||||
if (error.lockedOut) {
|
||||
return { success: false, error: 'Too many failed attempts. Try again in ' + Math.ceil((error.lockoutSeconds || 900) / 60) + ' minutes.' };
|
||||
}
|
||||
if (error.usernameTaken) {
|
||||
return { success: false, usernameTaken: true, error: error.message || error.toString() };
|
||||
}
|
||||
if (error.nameLocked) {
|
||||
return { success: false, nameLocked: true, registeredName: error.registeredName, error: error.message };
|
||||
}
|
||||
return { success: false, error: error.message || error.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('install-game', async (event, playerName, javaPath, installPath, branch) => {
|
||||
try {
|
||||
console.log(`[IPC] install-game called with parameters:`);
|
||||
@@ -1289,6 +1384,11 @@ ipcMain.handle('get-detected-gpu', () => {
|
||||
ipcMain.handle('save-version-branch', (event, branch) => {
|
||||
const { saveVersionBranch } = require('./backend/launcher');
|
||||
saveVersionBranch(branch);
|
||||
// Sync to active profile
|
||||
const activeProfile = profileManager.getActiveProfile();
|
||||
if (activeProfile) {
|
||||
profileManager.updateProfile(activeProfile.id, { versionBranch: branch });
|
||||
}
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -1302,6 +1402,43 @@ ipcMain.handle('load-version-client', () => {
|
||||
return loadVersionClient();
|
||||
});
|
||||
|
||||
ipcMain.handle('get-game-info', async () => {
|
||||
const { loadVersionClient, loadVersionBranch } = require('./backend/launcher');
|
||||
const { fetchMirrorManifest } = require('./backend/services/versionManager');
|
||||
const config = loadConfig();
|
||||
const branch = loadVersionBranch();
|
||||
|
||||
let version = null;
|
||||
let readableVersion = null;
|
||||
try {
|
||||
const manifest = await fetchMirrorManifest();
|
||||
if (manifest?.versions?.[branch]) {
|
||||
const branchVersions = manifest.versions[branch];
|
||||
// Get the highest version number for the current branch
|
||||
const nums = Object.keys(branchVersions).map(Number).filter(n => !isNaN(n));
|
||||
if (nums.length > 0) {
|
||||
const latest = Math.max(...nums).toString();
|
||||
version = `v${latest}`;
|
||||
readableVersion = branchVersions[latest]?.version || null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Manifest fetch failed, fall back to stored version
|
||||
version = loadVersionClient();
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
version = loadVersionClient();
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
readableVersion,
|
||||
branch,
|
||||
lastPlayed: config.last_played || null
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.handle('window-close', () => {
|
||||
app.quit();
|
||||
});
|
||||
@@ -1352,9 +1489,12 @@ ipcMain.handle('get-all-uuid-mappings', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('set-uuid-for-user', async (event, username, uuid) => {
|
||||
ipcMain.handle('set-uuid-for-user', async (event, username, uuid, force) => {
|
||||
try {
|
||||
await setUuidForUser(username, uuid);
|
||||
const result = setUuidForUser(username, uuid, { force: !!force });
|
||||
if (result && result.success === false) {
|
||||
return result; // { success: false, error: 'duplicate', existingUuid }
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error setting UUID for user:', error);
|
||||
@@ -1391,6 +1531,88 @@ ipcMain.handle('reset-current-user-uuid', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Password Management IPC handlers
|
||||
ipcMain.handle('check-password-status', async (event, uuid) => {
|
||||
try {
|
||||
const { getAuthServerUrl } = require('./backend/core/config');
|
||||
const authServerUrl = getAuthServerUrl();
|
||||
const response = await fetch(`${authServerUrl}/player/password/status/${uuid}`);
|
||||
if (!response.ok) return { hasPassword: false };
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error checking password status:', error);
|
||||
return { hasPassword: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('set-player-password', async (event, uuid, password, currentPassword) => {
|
||||
try {
|
||||
const { getAuthServerUrl } = require('./backend/core/config');
|
||||
const { getUuidForUser, loadUsername } = require('./backend/core/config');
|
||||
const authServerUrl = getAuthServerUrl();
|
||||
// First get a bearer token for auth
|
||||
const name = loadUsername() || 'Player';
|
||||
const tokenResp = await fetch(`${authServerUrl}/game-session/child`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ uuid, name, password: currentPassword || undefined })
|
||||
});
|
||||
if (!tokenResp.ok) {
|
||||
const err = await tokenResp.json().catch(() => ({}));
|
||||
return { success: false, error: err.error || 'Failed to authenticate' };
|
||||
}
|
||||
const tokenData = await tokenResp.json();
|
||||
const bearerToken = tokenData.identityToken || tokenData.IdentityToken;
|
||||
|
||||
const response = await fetch(`${authServerUrl}/player/password/set`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${bearerToken}`
|
||||
},
|
||||
body: JSON.stringify({ uuid, password, currentPassword: currentPassword || undefined })
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error setting password:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('remove-player-password', async (event, uuid, currentPassword) => {
|
||||
try {
|
||||
const { getAuthServerUrl } = require('./backend/core/config');
|
||||
const { loadUsername } = require('./backend/core/config');
|
||||
const authServerUrl = getAuthServerUrl();
|
||||
const name = loadUsername() || 'Player';
|
||||
// Get bearer token with current password
|
||||
const tokenResp = await fetch(`${authServerUrl}/game-session/child`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ uuid, name, password: currentPassword })
|
||||
});
|
||||
if (!tokenResp.ok) {
|
||||
const err = await tokenResp.json().catch(() => ({}));
|
||||
return { success: false, error: err.error || 'Failed to authenticate' };
|
||||
}
|
||||
const tokenData = await tokenResp.json();
|
||||
const bearerToken = tokenData.identityToken || tokenData.IdentityToken;
|
||||
|
||||
const response = await fetch(`${authServerUrl}/player/password/remove`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${bearerToken}`
|
||||
},
|
||||
body: JSON.stringify({ uuid, currentPassword })
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error removing password:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => {
|
||||
try {
|
||||
const logDir = logger.getLogDirectory();
|
||||
@@ -1597,6 +1819,108 @@ ipcMain.handle('preview-wrapper-script', (event, config, platform) => {
|
||||
return generateWrapperScript(config || require('./backend/launcher').loadWrapperConfig(), platform || process.platform, '/path/to/java');
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// MATCHA SOCIAL IPC HANDLERS
|
||||
// =============================================================================
|
||||
|
||||
ipcMain.handle('matcha:log', (event, level, ...args) => {
|
||||
const prefix = '[Matcha/Renderer]';
|
||||
if (level === 'error') console.error(prefix, ...args);
|
||||
else if (level === 'warn') console.warn(prefix, ...args);
|
||||
else console.log(prefix, ...args);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:register', async (event, username, password, password2) => {
|
||||
return matchaService.register(username, password, password2);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:confirm-register', async (event, pendingId, proofId) => {
|
||||
return matchaService.confirmRegistration(pendingId, proofId);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:login', async (event, handle, password) => {
|
||||
return matchaService.login(handle, password);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:logout', async () => {
|
||||
return matchaService.logout();
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:get-auth-state', () => {
|
||||
return matchaService.getAuthState();
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:get-me', async () => {
|
||||
return matchaService.getMe();
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:get-user', async (event, userId) => {
|
||||
return matchaService.getUser(userId);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:get-friends', async () => {
|
||||
return matchaService.getFriends();
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:friend-request', async (event, handle) => {
|
||||
return matchaService.sendFriendRequest(handle);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:friend-accept', async (event, requestId) => {
|
||||
return matchaService.acceptFriend(requestId);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:friend-reject', async (event, requestId) => {
|
||||
return matchaService.rejectFriend(requestId);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:friend-cancel', async (event, requestId) => {
|
||||
return matchaService.cancelFriendRequest(requestId);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:friend-remove', async (event, friendId) => {
|
||||
return matchaService.removeFriend(friendId);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:get-messages', async (event, withTarget, cursor, after) => {
|
||||
return matchaService.getMessages(withTarget, cursor, after);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:send-message', async (event, to, body, replyTo) => {
|
||||
return matchaService.sendMessage(to, body, replyTo);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:delete-message', async (event, messageId) => {
|
||||
return matchaService.deleteMessage(messageId);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:get-unread', async () => {
|
||||
return matchaService.getUnread();
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:clear-unread', async (event, withTarget) => {
|
||||
return matchaService.clearUnread(withTarget);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:upload-avatar', async (event, mode) => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Select Avatar Image',
|
||||
filters: [{ name: 'PNG Images', extensions: ['png'] }],
|
||||
properties: ['openFile']
|
||||
});
|
||||
if (result.canceled || !result.filePaths[0]) return { ok: false, error: 'Cancelled' };
|
||||
return matchaService.uploadAvatar(result.filePaths[0], mode);
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:delete-avatar', async () => {
|
||||
return matchaService.deleteAvatar();
|
||||
});
|
||||
|
||||
ipcMain.handle('matcha:reconnect', () => {
|
||||
matchaService.manualReconnect();
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
ipcMain.handle('get-current-platform', () => {
|
||||
return process.platform;
|
||||
});
|
||||
|
||||
54
package-lock.json
generated
54
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hytale-f2p-launcher",
|
||||
"version": "2.4.2",
|
||||
"version": "2.4.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hytale-f2p-launcher",
|
||||
"version": "2.4.2",
|
||||
"version": "2.4.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.10",
|
||||
@@ -17,7 +17,8 @@
|
||||
"encoding": "^0.1.13",
|
||||
"fs-extra": "^11.3.3",
|
||||
"tar": "^7.5.7",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^9.0.1",
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^40.0.0",
|
||||
@@ -335,6 +336,7 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cross-dirname": "^0.1.0",
|
||||
"debug": "^4.3.4",
|
||||
@@ -789,7 +791,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -1592,7 +1593,8 @@
|
||||
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
@@ -1803,6 +1805,27 @@
|
||||
"register-scheme": "github:devsnek/node-register-scheme"
|
||||
}
|
||||
},
|
||||
"node_modules/discord-rpc/node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/dmg-builder": {
|
||||
"version": "26.6.0",
|
||||
"resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.6.0.tgz",
|
||||
@@ -2094,6 +2117,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@electron/asar": "^3.2.1",
|
||||
"debug": "^4.1.1",
|
||||
@@ -2114,6 +2138,7 @@
|
||||
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.1.2",
|
||||
"jsonfile": "^4.0.0",
|
||||
@@ -2129,6 +2154,7 @@
|
||||
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"optionalDependencies": {
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
@@ -2139,6 +2165,7 @@
|
||||
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
@@ -2155,7 +2182,6 @@
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.6.2"
|
||||
}
|
||||
@@ -3399,6 +3425,7 @@
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
@@ -3751,7 +3778,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -3781,6 +3807,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"commander": "^9.4.0"
|
||||
},
|
||||
@@ -3798,6 +3825,7 @@
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
@@ -4006,6 +4034,7 @@
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
@@ -4403,6 +4432,7 @@
|
||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
@@ -4712,16 +4742,16 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hytale-f2p-launcher",
|
||||
"version": "2.4.5",
|
||||
"version": "2.4.8",
|
||||
"description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support",
|
||||
"homepage": "https://git.sanhost.net/sanasol/hytale-f2p",
|
||||
"main": "main.js",
|
||||
@@ -56,7 +56,8 @@
|
||||
"electron-updater": "^6.7.3",
|
||||
"fs-extra": "^11.3.3",
|
||||
"tar": "^7.5.7",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^9.0.1",
|
||||
"ws": "^8.16.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.hytalef2p.launcher",
|
||||
|
||||
47
preload.js
47
preload.js
@@ -2,6 +2,7 @@ const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
launchGame: (playerName, javaPath, installPath, gpuPreference) => ipcRenderer.invoke('launch-game', playerName, javaPath, installPath, gpuPreference),
|
||||
launchGameWithPassword: (playerName, javaPath, installPath, gpuPreference, password) => ipcRenderer.invoke('launch-game-with-password', playerName, javaPath, installPath, gpuPreference, password),
|
||||
installGame: (playerName, javaPath, installPath, branch) => ipcRenderer.invoke('install-game', playerName, javaPath, installPath, branch),
|
||||
closeWindow: () => ipcRenderer.invoke('window-close'),
|
||||
minimizeWindow: () => ipcRenderer.invoke('window-minimize'),
|
||||
@@ -78,6 +79,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
saveVersionBranch: (branch) => ipcRenderer.invoke('save-version-branch', branch),
|
||||
loadVersionBranch: () => ipcRenderer.invoke('load-version-branch'),
|
||||
loadVersionClient: () => ipcRenderer.invoke('load-version-client'),
|
||||
getGameInfo: () => ipcRenderer.invoke('get-game-info'),
|
||||
|
||||
acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame),
|
||||
markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'),
|
||||
@@ -102,11 +104,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// 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),
|
||||
setUuidForUser: (username, uuid, force) => ipcRenderer.invoke('set-uuid-for-user', username, uuid, force),
|
||||
generateNewUuid: () => ipcRenderer.invoke('generate-new-uuid'),
|
||||
deleteUuidForUser: (username) => ipcRenderer.invoke('delete-uuid-for-user', username),
|
||||
resetCurrentUserUuid: () => ipcRenderer.invoke('reset-current-user-uuid'),
|
||||
|
||||
// Password Management methods
|
||||
checkPasswordStatus: (uuid) => ipcRenderer.invoke('check-password-status', uuid),
|
||||
setPlayerPassword: (uuid, password, currentPassword) => ipcRenderer.invoke('set-player-password', uuid, password, currentPassword),
|
||||
removePlayerPassword: (uuid, currentPassword) => ipcRenderer.invoke('remove-player-password', uuid, currentPassword),
|
||||
promptPassword: () => ipcRenderer.invoke('prompt-password'),
|
||||
onPasswordPrompt: (callback) => {
|
||||
ipcRenderer.on('show-password-prompt', (event, data) => callback(data));
|
||||
},
|
||||
|
||||
// Java Wrapper Config API
|
||||
loadWrapperConfig: () => ipcRenderer.invoke('load-wrapper-config'),
|
||||
saveWrapperConfig: (config) => ipcRenderer.invoke('save-wrapper-config', config),
|
||||
@@ -142,5 +153,39 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
},
|
||||
onUpdateError: (callback) => {
|
||||
ipcRenderer.on('update-error', (event, data) => callback(data));
|
||||
},
|
||||
|
||||
// Matcha Social API
|
||||
matcha: {
|
||||
log: (level, ...args) => ipcRenderer.invoke('matcha:log', level, ...args),
|
||||
register: (username, password, password2) => ipcRenderer.invoke('matcha:register', username, password, password2),
|
||||
confirmRegister: (pendingId, proofId) => ipcRenderer.invoke('matcha:confirm-register', pendingId, proofId),
|
||||
login: (handle, password) => ipcRenderer.invoke('matcha:login', handle, password),
|
||||
logout: () => ipcRenderer.invoke('matcha:logout'),
|
||||
getAuthState: () => ipcRenderer.invoke('matcha:get-auth-state'),
|
||||
getMe: () => ipcRenderer.invoke('matcha:get-me'),
|
||||
getUser: (userId) => ipcRenderer.invoke('matcha:get-user', userId),
|
||||
getFriends: () => ipcRenderer.invoke('matcha:get-friends'),
|
||||
friendRequest: (handle) => ipcRenderer.invoke('matcha:friend-request', handle),
|
||||
friendAccept: (requestId) => ipcRenderer.invoke('matcha:friend-accept', requestId),
|
||||
friendReject: (requestId) => ipcRenderer.invoke('matcha:friend-reject', requestId),
|
||||
friendCancel: (requestId) => ipcRenderer.invoke('matcha:friend-cancel', requestId),
|
||||
friendRemove: (friendId) => ipcRenderer.invoke('matcha:friend-remove', friendId),
|
||||
getMessages: (withTarget, cursor, after) => ipcRenderer.invoke('matcha:get-messages', withTarget, cursor, after),
|
||||
sendMessage: (to, body, replyTo) => ipcRenderer.invoke('matcha:send-message', to, body, replyTo),
|
||||
deleteMessage: (messageId) => ipcRenderer.invoke('matcha:delete-message', messageId),
|
||||
getUnread: () => ipcRenderer.invoke('matcha:get-unread'),
|
||||
clearUnread: (withTarget) => ipcRenderer.invoke('matcha:clear-unread', withTarget),
|
||||
uploadAvatar: (mode) => ipcRenderer.invoke('matcha:upload-avatar', mode),
|
||||
deleteAvatar: () => ipcRenderer.invoke('matcha:delete-avatar'),
|
||||
reconnect: () => ipcRenderer.invoke('matcha:reconnect'),
|
||||
onWsMessage: (callback) => ipcRenderer.on('matcha:ws:message', (event, data) => callback(data)),
|
||||
onWsConnected: (callback) => ipcRenderer.on('matcha:ws:connected', (event, data) => callback(data)),
|
||||
onWsDisconnected: (callback) => ipcRenderer.on('matcha:ws:disconnected', () => callback()),
|
||||
onMessageDeleted: (callback) => ipcRenderer.on('matcha:ws:message-deleted', (event, data) => callback(data)),
|
||||
onAvatarUpdated: (callback) => ipcRenderer.on('matcha:ws:avatar-updated', (event, data) => callback(data)),
|
||||
onBanned: (callback) => ipcRenderer.on('matcha:ws:banned', (event, data) => callback(data)),
|
||||
onAnnouncement: (callback) => ipcRenderer.on('matcha:ws:announcement', (event, data) => callback(data)),
|
||||
onMaxRetries: (callback) => ipcRenderer.on('matcha:ws:max-retries', () => callback())
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user