mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-26 14:51:48 -03:00
Compare commits
4 Commits
v2.3.8
...
fix/uuid-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14dcf3dac3 | ||
|
|
34a3e40bd2 | ||
|
|
d15f2e2ceb | ||
|
|
74f99d0aaf |
@@ -194,27 +194,81 @@ window.switchProfile = async (id) => {
|
|||||||
export async function launch() {
|
export async function launch() {
|
||||||
if (isDownloading || (playBtn && playBtn.disabled)) return;
|
if (isDownloading || (playBtn && playBtn.disabled)) return;
|
||||||
|
|
||||||
let playerName = 'Player';
|
// ==========================================================================
|
||||||
if (window.SettingsAPI && window.SettingsAPI.getCurrentPlayerName) {
|
// STEP 1: Check launch readiness from backend (single source of truth)
|
||||||
playerName = window.SettingsAPI.getCurrentPlayerName();
|
// ==========================================================================
|
||||||
} else if (playerNameInput && playerNameInput.value.trim()) {
|
let launchState = null;
|
||||||
playerName = playerNameInput.value.trim();
|
let playerName = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (window.electronAPI && window.electronAPI.checkLaunchReady) {
|
||||||
|
launchState = await window.electronAPI.checkLaunchReady();
|
||||||
|
playerName = launchState?.username;
|
||||||
|
} else if (window.electronAPI && window.electronAPI.loadUsername) {
|
||||||
|
// Fallback to loadUsername if checkLaunchReady not available
|
||||||
|
playerName = await window.electronAPI.loadUsername();
|
||||||
|
launchState = { ready: !!playerName, hasUsername: !!playerName, username: playerName, issues: [] };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Launcher] Error checking launch readiness:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
let javaPath = '';
|
// Validate launch readiness
|
||||||
if (window.SettingsAPI && window.SettingsAPI.getCurrentJavaPath) {
|
if (!launchState?.ready || !playerName) {
|
||||||
javaPath = window.SettingsAPI.getCurrentJavaPath();
|
const issues = launchState?.issues || ['No username configured'];
|
||||||
|
const errorMsg = window.i18n
|
||||||
|
? window.i18n.t('errors.noUsername')
|
||||||
|
: 'Please set your username in Settings before playing.';
|
||||||
|
|
||||||
|
console.error('[Launcher] Launch blocked:', issues.join(', '));
|
||||||
|
|
||||||
|
// Show error to user
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||||
|
window.LauncherUI.showError(errorMsg);
|
||||||
|
} else {
|
||||||
|
alert(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to settings if possible
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showPage) {
|
||||||
|
window.LauncherUI.showPage('settings-page');
|
||||||
|
window.LauncherUI.setActiveNav('settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Warn if using default 'Player' name (shouldn't happen with new logic, but keep as safety)
|
||||||
|
if (playerName === 'Player') {
|
||||||
|
console.warn('[Launcher] Warning: Using default username "Player"');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Launcher] Launching game for: "${playerName}"`);
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// STEP 2: Load other settings from backend
|
||||||
|
// ==========================================================================
|
||||||
|
let javaPath = '';
|
||||||
|
try {
|
||||||
|
if (window.electronAPI && window.electronAPI.loadJavaPath) {
|
||||||
|
javaPath = await window.electronAPI.loadJavaPath() || '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Launcher] Error loading Java path:', error);
|
||||||
|
}
|
||||||
|
|
||||||
let gpuPreference = 'auto';
|
let gpuPreference = 'auto';
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.loadGpuPreference) {
|
if (window.electronAPI && window.electronAPI.loadGpuPreference) {
|
||||||
gpuPreference = await window.electronAPI.loadGpuPreference();
|
gpuPreference = await window.electronAPI.loadGpuPreference();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading GPU preference:', error);
|
console.error('[Launcher] Error loading GPU preference:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// STEP 3: Start launch process
|
||||||
|
// ==========================================================================
|
||||||
if (window.LauncherUI) window.LauncherUI.showProgress();
|
if (window.LauncherUI) window.LauncherUI.showProgress();
|
||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
if (playBtn) {
|
if (playBtn) {
|
||||||
@@ -227,8 +281,9 @@ export async function launch() {
|
|||||||
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: startingMsg });
|
if (window.LauncherUI) window.LauncherUI.updateProgress({ message: startingMsg });
|
||||||
|
|
||||||
if (window.electronAPI && window.electronAPI.launchGame) {
|
if (window.electronAPI && window.electronAPI.launchGame) {
|
||||||
|
// Pass playerName from config - backend will validate again
|
||||||
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
|
const result = await window.electronAPI.launchGame(playerName, javaPath, '', gpuPreference);
|
||||||
|
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
|
|
||||||
if (window.LauncherUI) {
|
if (window.LauncherUI) {
|
||||||
@@ -243,7 +298,35 @@ export async function launch() {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Launch failed:', result.error);
|
console.error('[Launcher] Launch failed:', result.error);
|
||||||
|
|
||||||
|
// Handle specific error cases
|
||||||
|
if (result.needsUsername) {
|
||||||
|
const errorMsg = window.i18n
|
||||||
|
? window.i18n.t('errors.noUsername')
|
||||||
|
: 'Please set your username in Settings before playing.';
|
||||||
|
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||||
|
window.LauncherUI.showError(errorMsg);
|
||||||
|
} else {
|
||||||
|
alert(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to settings
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showPage) {
|
||||||
|
window.LauncherUI.showPage('settings-page');
|
||||||
|
window.LauncherUI.setActiveNav('settings');
|
||||||
|
}
|
||||||
|
} else if (result.error) {
|
||||||
|
// Show generic error
|
||||||
|
const errorMsg = window.i18n
|
||||||
|
? window.i18n.t('errors.launchFailed').replace('{error}', result.error)
|
||||||
|
: `Launch failed: ${result.error}`;
|
||||||
|
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||||
|
window.LauncherUI.showError(errorMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
isDownloading = false;
|
isDownloading = false;
|
||||||
@@ -260,7 +343,13 @@ export async function launch() {
|
|||||||
window.LauncherUI.hideProgress();
|
window.LauncherUI.hideProgress();
|
||||||
}
|
}
|
||||||
resetPlayButton();
|
resetPlayButton();
|
||||||
console.error('Launch error:', error);
|
console.error('[Launcher] Launch error:', error);
|
||||||
|
|
||||||
|
// Show error to user
|
||||||
|
const errorMsg = error.message || 'Unknown launch error';
|
||||||
|
if (window.LauncherUI && window.LauncherUI.showError) {
|
||||||
|
window.LauncherUI.showError(errorMsg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -446,10 +446,27 @@ async function savePlayerName() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.electronAPI.saveUsername(playerName);
|
const result = await window.electronAPI.saveUsername(playerName);
|
||||||
|
|
||||||
|
// Check if save was successful
|
||||||
|
if (result && result.success === false) {
|
||||||
|
console.error('[Settings] Failed to save username:', result.error);
|
||||||
|
const errorMsg = window.i18n
|
||||||
|
? window.i18n.t('notifications.playerNameSaveFailed')
|
||||||
|
: `Failed to save player name: ${result.error || 'Unknown error'}`;
|
||||||
|
showNotification(errorMsg, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully';
|
const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully';
|
||||||
showNotification(successMsg, 'success');
|
showNotification(successMsg, 'success');
|
||||||
|
|
||||||
|
// Refresh UUID display since it may have changed for the new username
|
||||||
|
await loadCurrentUuid();
|
||||||
|
|
||||||
|
// Also refresh the UUID list to update which entry is marked as current
|
||||||
|
await loadAllUuids();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving player name:', error);
|
console.error('Error saving player name:', error);
|
||||||
const errorMsg = window.i18n ? window.i18n.t('notifications.playerNameSaveFailed') : 'Failed to save player name';
|
const errorMsg = window.i18n ? window.i18n.t('notifications.playerNameSaveFailed') : 'Failed to save player name';
|
||||||
@@ -573,11 +590,26 @@ export function getCurrentJavaPath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current player name from UI input
|
||||||
|
* Returns null if no name is set (caller must handle this)
|
||||||
|
* NOTE: launcher.js now loads username directly from backend config
|
||||||
|
* This function is used for display purposes only
|
||||||
|
*/
|
||||||
export function getCurrentPlayerName() {
|
export function getCurrentPlayerName() {
|
||||||
if (settingsPlayerName && settingsPlayerName.value.trim()) {
|
if (settingsPlayerName && settingsPlayerName.value.trim()) {
|
||||||
return settingsPlayerName.value.trim();
|
return settingsPlayerName.value.trim();
|
||||||
}
|
}
|
||||||
return 'Player';
|
// Return null instead of 'Player' - caller must handle missing username
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current player name with fallback for display purposes only
|
||||||
|
* DO NOT use this for launching game - use backend loadUsername() instead
|
||||||
|
*/
|
||||||
|
export function getCurrentPlayerNameForDisplay() {
|
||||||
|
return getCurrentPlayerName() || 'Player';
|
||||||
}
|
}
|
||||||
|
|
||||||
window.openGameLocation = openGameLocation;
|
window.openGameLocation = openGameLocation;
|
||||||
@@ -587,6 +619,7 @@ document.addEventListener('DOMContentLoaded', initSettings);
|
|||||||
window.SettingsAPI = {
|
window.SettingsAPI = {
|
||||||
getCurrentJavaPath,
|
getCurrentJavaPath,
|
||||||
getCurrentPlayerName,
|
getCurrentPlayerName,
|
||||||
|
getCurrentPlayerNameForDisplay,
|
||||||
reloadBranch: loadVersionBranch
|
reloadBranch: loadVersionBranch
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -729,6 +762,9 @@ async function loadAllUuids() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="uuid-item-actions">
|
<div class="uuid-item-actions">
|
||||||
${mapping.isCurrent ? '<div class="uuid-item-current-badge">Current</div>' : ''}
|
${mapping.isCurrent ? '<div class="uuid-item-current-badge">Current</div>' : ''}
|
||||||
|
${!mapping.isCurrent ? `<button class="uuid-item-btn switch" onclick="switchToUsername('${escapeHtml(mapping.username)}')" title="Switch to this identity">
|
||||||
|
<i class="fas fa-user-check"></i>
|
||||||
|
</button>` : ''}
|
||||||
<button class="uuid-item-btn copy" onclick="copyUuid('${mapping.uuid}')" title="Copy UUID">
|
<button class="uuid-item-btn copy" onclick="copyUuid('${mapping.uuid}')" title="Copy UUID">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -813,7 +849,17 @@ async function setCustomUuid() {
|
|||||||
async function performSetCustomUuid(uuid) {
|
async function performSetCustomUuid(uuid) {
|
||||||
try {
|
try {
|
||||||
if (window.electronAPI && window.electronAPI.setUuidForUser) {
|
if (window.electronAPI && window.electronAPI.setUuidForUser) {
|
||||||
const username = getCurrentPlayerName();
|
// IMPORTANT: Use saved username from config, not unsaved DOM input
|
||||||
|
// This prevents setting UUID for wrong user if username field was edited but not saved
|
||||||
|
let username = null;
|
||||||
|
if (window.electronAPI.loadUsername) {
|
||||||
|
username = await window.electronAPI.loadUsername();
|
||||||
|
}
|
||||||
|
if (!username) {
|
||||||
|
const msg = window.i18n ? window.i18n.t('notifications.noUsername') : 'No username configured. Please save your username first.';
|
||||||
|
showNotification(msg, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const result = await window.electronAPI.setUuidForUser(username, uuid);
|
const result = await window.electronAPI.setUuidForUser(username, uuid);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -850,6 +896,73 @@ window.copyUuid = async function (uuid) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch to a different username/UUID identity
|
||||||
|
* This changes the active username to use that username's UUID
|
||||||
|
*/
|
||||||
|
window.switchToUsername = async function (username) {
|
||||||
|
try {
|
||||||
|
const message = window.i18n
|
||||||
|
? window.i18n.t('confirm.switchUsernameMessage').replace('{username}', username)
|
||||||
|
: `Switch to username "${username}"? This will change your active player identity.`;
|
||||||
|
const title = window.i18n ? window.i18n.t('confirm.switchUsernameTitle') : 'Switch Identity';
|
||||||
|
const confirmBtn = window.i18n ? window.i18n.t('confirm.switchUsernameButton') : 'Switch';
|
||||||
|
const cancelBtn = window.i18n ? window.i18n.t('common.cancel') : 'Cancel';
|
||||||
|
|
||||||
|
showCustomConfirm(
|
||||||
|
message,
|
||||||
|
title,
|
||||||
|
async () => {
|
||||||
|
await performSwitchToUsername(username);
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
confirmBtn,
|
||||||
|
cancelBtn
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in switchToUsername:', error);
|
||||||
|
const msg = window.i18n ? window.i18n.t('notifications.switchUsernameFailed') : 'Failed to switch username';
|
||||||
|
showNotification(msg, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function performSwitchToUsername(username) {
|
||||||
|
try {
|
||||||
|
if (!window.electronAPI || !window.electronAPI.saveUsername) {
|
||||||
|
throw new Error('API not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await window.electronAPI.saveUsername(username);
|
||||||
|
|
||||||
|
if (result && result.success === false) {
|
||||||
|
throw new Error(result.error || 'Failed to save username');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the username input field
|
||||||
|
if (settingsPlayerName) {
|
||||||
|
settingsPlayerName.value = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the current UUID display
|
||||||
|
await loadCurrentUuid();
|
||||||
|
|
||||||
|
// Refresh the UUID list to show new "Current" badge
|
||||||
|
await loadAllUuids();
|
||||||
|
|
||||||
|
const msg = window.i18n
|
||||||
|
? window.i18n.t('notifications.switchUsernameSuccess').replace('{username}', username)
|
||||||
|
: `Switched to "${username}" successfully!`;
|
||||||
|
showNotification(msg, 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error switching username:', error);
|
||||||
|
const msg = window.i18n
|
||||||
|
? window.i18n.t('notifications.switchUsernameFailed')
|
||||||
|
: `Failed to switch username: ${error.message}`;
|
||||||
|
showNotification(msg, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.deleteUuid = async function (username) {
|
window.deleteUuid = async function (username) {
|
||||||
try {
|
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 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.`;
|
||||||
|
|||||||
@@ -211,7 +211,11 @@
|
|||||||
"modsDeleteFailed": "Mod konnte nicht gelöscht werden: {error}",
|
"modsDeleteFailed": "Mod konnte nicht gelöscht werden: {error}",
|
||||||
"modsModNotFound": "Mod-Informationen nicht gefunden",
|
"modsModNotFound": "Mod-Informationen nicht gefunden",
|
||||||
"hwAccelSaved": "Hardware-Beschleunigungseinstellung gespeichert",
|
"hwAccelSaved": "Hardware-Beschleunigungseinstellung gespeichert",
|
||||||
"hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden"
|
"hwAccelSaveFailed": "Hardware-Beschleunigungseinstellung konnte nicht gespeichert werden",
|
||||||
|
"noUsername": "Kein Benutzername konfiguriert. Bitte speichere zuerst deinen Benutzernamen.",
|
||||||
|
"switchUsernameSuccess": "Erfolgreich zu \"{username}\" gewechselt!",
|
||||||
|
"switchUsernameFailed": "Benutzername konnte nicht gewechselt werden",
|
||||||
|
"playerNameTooLong": "Spielername darf maximal 16 Zeichen haben"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Aktion bestätigen",
|
"defaultTitle": "Aktion bestätigen",
|
||||||
@@ -226,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Löschen",
|
"deleteUuidButton": "Löschen",
|
||||||
"uninstallGameTitle": "Spiel deinstallieren",
|
"uninstallGameTitle": "Spiel deinstallieren",
|
||||||
"uninstallGameMessage": "Möchtest du Hytale wirklich deinstallieren? Alle Spieldateien werden gelöscht.",
|
"uninstallGameMessage": "Möchtest du Hytale wirklich deinstallieren? Alle Spieldateien werden gelöscht.",
|
||||||
"uninstallGameButton": "Deinstallieren"
|
"uninstallGameButton": "Deinstallieren",
|
||||||
|
"switchUsernameTitle": "Identität wechseln",
|
||||||
|
"switchUsernameMessage": "Zu Benutzername \"{username}\" wechseln? Dies ändert deine aktuelle Spieleridentität.",
|
||||||
|
"switchUsernameButton": "Wechseln"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Initialisiere...",
|
"initializing": "Initialisiere...",
|
||||||
|
|||||||
@@ -211,7 +211,11 @@
|
|||||||
"modsDeleteFailed": "Failed to delete mod: {error}",
|
"modsDeleteFailed": "Failed to delete mod: {error}",
|
||||||
"modsModNotFound": "Mod information not found",
|
"modsModNotFound": "Mod information not found",
|
||||||
"hwAccelSaved": "Hardware acceleration setting saved",
|
"hwAccelSaved": "Hardware acceleration setting saved",
|
||||||
"hwAccelSaveFailed": "Failed to save hardware acceleration setting"
|
"hwAccelSaveFailed": "Failed to save hardware acceleration setting",
|
||||||
|
"noUsername": "No username configured. Please save your username first.",
|
||||||
|
"switchUsernameSuccess": "Switched to \"{username}\" successfully!",
|
||||||
|
"switchUsernameFailed": "Failed to switch username",
|
||||||
|
"playerNameTooLong": "Player name must be 16 characters or less"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Confirm action",
|
"defaultTitle": "Confirm action",
|
||||||
@@ -226,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Delete",
|
"deleteUuidButton": "Delete",
|
||||||
"uninstallGameTitle": "Uninstall game",
|
"uninstallGameTitle": "Uninstall game",
|
||||||
"uninstallGameMessage": "Are you sure you want to uninstall Hytale? All game files will be deleted.",
|
"uninstallGameMessage": "Are you sure you want to uninstall Hytale? All game files will be deleted.",
|
||||||
"uninstallGameButton": "Uninstall"
|
"uninstallGameButton": "Uninstall",
|
||||||
|
"switchUsernameTitle": "Switch Identity",
|
||||||
|
"switchUsernameMessage": "Switch to username \"{username}\"? This will change your current player identity.",
|
||||||
|
"switchUsernameButton": "Switch"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Initializing...",
|
"initializing": "Initializing...",
|
||||||
|
|||||||
@@ -211,7 +211,11 @@
|
|||||||
"modsDeleteFailed": "Error al eliminar mod: {error}",
|
"modsDeleteFailed": "Error al eliminar mod: {error}",
|
||||||
"modsModNotFound": "Información del mod no encontrada",
|
"modsModNotFound": "Información del mod no encontrada",
|
||||||
"hwAccelSaved": "Configuración de aceleración por hardware guardada",
|
"hwAccelSaved": "Configuración de aceleración por hardware guardada",
|
||||||
"hwAccelSaveFailed": "Error al guardar la configuración de aceleración por hardware"
|
"hwAccelSaveFailed": "Error al guardar la configuración de aceleración por hardware",
|
||||||
|
"noUsername": "No hay nombre de usuario configurado. Por favor, guarda tu nombre de usuario primero.",
|
||||||
|
"switchUsernameSuccess": "¡Cambiado a \"{username}\" con éxito!",
|
||||||
|
"switchUsernameFailed": "Error al cambiar nombre de usuario",
|
||||||
|
"playerNameTooLong": "El nombre del jugador debe tener 16 caracteres o menos"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Confirmar acción",
|
"defaultTitle": "Confirmar acción",
|
||||||
@@ -226,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Eliminar",
|
"deleteUuidButton": "Eliminar",
|
||||||
"uninstallGameTitle": "Desinstalar juego",
|
"uninstallGameTitle": "Desinstalar juego",
|
||||||
"uninstallGameMessage": "¿Estás seguro de que quieres desinstalar Hytale? Se eliminarán todos los archivos del juego.",
|
"uninstallGameMessage": "¿Estás seguro de que quieres desinstalar Hytale? Se eliminarán todos los archivos del juego.",
|
||||||
"uninstallGameButton": "Desinstalar"
|
"uninstallGameButton": "Desinstalar",
|
||||||
|
"switchUsernameTitle": "Cambiar identidad",
|
||||||
|
"switchUsernameMessage": "¿Cambiar al nombre de usuario \"{username}\"? Esto cambiará tu identidad de jugador actual.",
|
||||||
|
"switchUsernameButton": "Cambiar"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Inicializando...",
|
"initializing": "Inicializando...",
|
||||||
|
|||||||
@@ -211,7 +211,11 @@
|
|||||||
"modsDeleteFailed": "Échec de la suppression du mod: {error}",
|
"modsDeleteFailed": "Échec de la suppression du mod: {error}",
|
||||||
"modsModNotFound": "Informations du mod introuvables",
|
"modsModNotFound": "Informations du mod introuvables",
|
||||||
"hwAccelSaved": "Paramètre d'accélération matérielle sauvegardé",
|
"hwAccelSaved": "Paramètre d'accélération matérielle sauvegardé",
|
||||||
"hwAccelSaveFailed": "Échec de la sauvegarde du paramètre d'accélération matérielle"
|
"hwAccelSaveFailed": "Échec de la sauvegarde du paramètre d'accélération matérielle",
|
||||||
|
"noUsername": "Aucun nom d'utilisateur configuré. Veuillez d'abord enregistrer votre nom d'utilisateur.",
|
||||||
|
"switchUsernameSuccess": "Basculé vers \"{username}\" avec succès!",
|
||||||
|
"switchUsernameFailed": "Échec du changement de nom d'utilisateur",
|
||||||
|
"playerNameTooLong": "Le nom du joueur doit comporter 16 caractères ou moins"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Confirmer l'action",
|
"defaultTitle": "Confirmer l'action",
|
||||||
@@ -226,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Supprimer",
|
"deleteUuidButton": "Supprimer",
|
||||||
"uninstallGameTitle": "Désinstaller le jeu",
|
"uninstallGameTitle": "Désinstaller le jeu",
|
||||||
"uninstallGameMessage": "Êtes-vous sûr de vouloir désinstaller Hytale? Tous les fichiers du jeu seront supprimés.",
|
"uninstallGameMessage": "Êtes-vous sûr de vouloir désinstaller Hytale? Tous les fichiers du jeu seront supprimés.",
|
||||||
"uninstallGameButton": "Désinstaller"
|
"uninstallGameButton": "Désinstaller",
|
||||||
|
"switchUsernameTitle": "Changer d'identité",
|
||||||
|
"switchUsernameMessage": "Basculer vers le nom d'utilisateur \"{username}\"? Cela changera votre identité de joueur actuelle.",
|
||||||
|
"switchUsernameButton": "Changer"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Initialisation...",
|
"initializing": "Initialisation...",
|
||||||
|
|||||||
@@ -211,7 +211,11 @@
|
|||||||
"modsDeleteFailed": "Gagal menghapus mod: {error}",
|
"modsDeleteFailed": "Gagal menghapus mod: {error}",
|
||||||
"modsModNotFound": "Informasi mod tidak ditemukan",
|
"modsModNotFound": "Informasi mod tidak ditemukan",
|
||||||
"hwAccelSaved": "Pengaturan akselerasi perangkat keras disimpan",
|
"hwAccelSaved": "Pengaturan akselerasi perangkat keras disimpan",
|
||||||
"hwAccelSaveFailed": "Gagal menyimpan pengaturan akselerasi perangkat keras"
|
"hwAccelSaveFailed": "Gagal menyimpan pengaturan akselerasi perangkat keras",
|
||||||
|
"noUsername": "Nama pengguna belum dikonfigurasi. Silakan simpan nama pengguna terlebih dahulu.",
|
||||||
|
"switchUsernameSuccess": "Berhasil beralih ke \"{username}\"!",
|
||||||
|
"switchUsernameFailed": "Gagal beralih nama pengguna",
|
||||||
|
"playerNameTooLong": "Nama pemain harus 16 karakter atau kurang"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Konfirmasi tindakan",
|
"defaultTitle": "Konfirmasi tindakan",
|
||||||
@@ -226,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Hapus",
|
"deleteUuidButton": "Hapus",
|
||||||
"uninstallGameTitle": "Hapus instalasi game",
|
"uninstallGameTitle": "Hapus instalasi game",
|
||||||
"uninstallGameMessage": "Apakah kamu yakin ingin menghapus instalasi Hytale? Semua file game akan dihapus.",
|
"uninstallGameMessage": "Apakah kamu yakin ingin menghapus instalasi Hytale? Semua file game akan dihapus.",
|
||||||
"uninstallGameButton": "Hapus Instalasi"
|
"uninstallGameButton": "Hapus Instalasi",
|
||||||
|
"switchUsernameTitle": "Ganti Identitas",
|
||||||
|
"switchUsernameMessage": "Beralih ke nama pengguna \"{username}\"? Ini akan mengubah identitas pemain saat ini.",
|
||||||
|
"switchUsernameButton": "Ganti"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Menginisialisasi...",
|
"initializing": "Menginisialisasi...",
|
||||||
|
|||||||
@@ -211,7 +211,11 @@
|
|||||||
"modsDeleteFailed": "Nie udało się usunąć moda: {error}",
|
"modsDeleteFailed": "Nie udało się usunąć moda: {error}",
|
||||||
"modsModNotFound": "Nie znaleziono informacji o modzie",
|
"modsModNotFound": "Nie znaleziono informacji o modzie",
|
||||||
"hwAccelSaved": "Zapisano ustawienie przyspieszenia sprzętowego",
|
"hwAccelSaved": "Zapisano ustawienie przyspieszenia sprzętowego",
|
||||||
"hwAccelSaveFailed": "Nie udało się zapisać ustawienia przyspieszenia sprzętowego"
|
"hwAccelSaveFailed": "Nie udało się zapisać ustawienia przyspieszenia sprzętowego",
|
||||||
|
"noUsername": "Nie skonfigurowano nazwy użytkownika. Najpierw zapisz swoją nazwę użytkownika.",
|
||||||
|
"switchUsernameSuccess": "Pomyślnie przełączono na \"{username}\"!",
|
||||||
|
"switchUsernameFailed": "Nie udało się przełączyć nazwy użytkownika",
|
||||||
|
"playerNameTooLong": "Nazwa gracza musi mieć 16 znaków lub mniej"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Potwierdź działanie",
|
"defaultTitle": "Potwierdź działanie",
|
||||||
@@ -226,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Usuń",
|
"deleteUuidButton": "Usuń",
|
||||||
"uninstallGameTitle": "Odinstaluj grę",
|
"uninstallGameTitle": "Odinstaluj grę",
|
||||||
"uninstallGameMessage": "Czy na pewno chcesz odinstalować Hytale? Wszystkie pliki gry zostaną usunięte.",
|
"uninstallGameMessage": "Czy na pewno chcesz odinstalować Hytale? Wszystkie pliki gry zostaną usunięte.",
|
||||||
"uninstallGameButton": "Odinstaluj"
|
"uninstallGameButton": "Odinstaluj",
|
||||||
|
"switchUsernameTitle": "Zmień tożsamość",
|
||||||
|
"switchUsernameMessage": "Przełączyć na nazwę użytkownika \"{username}\"? To zmieni Twoją aktualną tożsamość gracza.",
|
||||||
|
"switchUsernameButton": "Przełącz"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Inicjalizacja...",
|
"initializing": "Inicjalizacja...",
|
||||||
|
|||||||
@@ -211,7 +211,11 @@
|
|||||||
"modsDeleteFailed": "Falha ao excluir mod: {error}",
|
"modsDeleteFailed": "Falha ao excluir mod: {error}",
|
||||||
"modsModNotFound": "Informações do mod não encontradas",
|
"modsModNotFound": "Informações do mod não encontradas",
|
||||||
"hwAccelSaved": "Configuração de aceleração de hardware salva",
|
"hwAccelSaved": "Configuração de aceleração de hardware salva",
|
||||||
"hwAccelSaveFailed": "Falha ao salvar configuração de aceleração de hardware"
|
"hwAccelSaveFailed": "Falha ao salvar configuração de aceleração de hardware",
|
||||||
|
"noUsername": "Nenhum nome de usuário configurado. Por favor, salve seu nome de usuário primeiro.",
|
||||||
|
"switchUsernameSuccess": "Alterado para \"{username}\" com sucesso!",
|
||||||
|
"switchUsernameFailed": "Falha ao trocar nome de usuário",
|
||||||
|
"playerNameTooLong": "O nome do jogador deve ter 16 caracteres ou menos"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Confirmar ação",
|
"defaultTitle": "Confirmar ação",
|
||||||
@@ -226,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Excluir",
|
"deleteUuidButton": "Excluir",
|
||||||
"uninstallGameTitle": "Desinstalar jogo",
|
"uninstallGameTitle": "Desinstalar jogo",
|
||||||
"uninstallGameMessage": "Tem certeza de que deseja desinstalar Hytale? Todos os arquivos do jogo serão excluídos.",
|
"uninstallGameMessage": "Tem certeza de que deseja desinstalar Hytale? Todos os arquivos do jogo serão excluídos.",
|
||||||
"uninstallGameButton": "Desinstalar"
|
"uninstallGameButton": "Desinstalar",
|
||||||
|
"switchUsernameTitle": "Trocar Identidade",
|
||||||
|
"switchUsernameMessage": "Trocar para o nome de usuário \"{username}\"? Isso mudará sua identidade de jogador atual.",
|
||||||
|
"switchUsernameButton": "Trocar"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Inicializando...",
|
"initializing": "Inicializando...",
|
||||||
|
|||||||
@@ -211,7 +211,11 @@
|
|||||||
"modsDeleteFailed": "Не получилось удалить мод: {error}",
|
"modsDeleteFailed": "Не получилось удалить мод: {error}",
|
||||||
"modsModNotFound": "Информация по моду не найдена",
|
"modsModNotFound": "Информация по моду не найдена",
|
||||||
"hwAccelSaved": "Настройка аппаратного ускорения сохранена!",
|
"hwAccelSaved": "Настройка аппаратного ускорения сохранена!",
|
||||||
"hwAccelSaveFailed": "Не удалось сохранить настройку аппаратного ускорения"
|
"hwAccelSaveFailed": "Не удалось сохранить настройку аппаратного ускорения",
|
||||||
|
"noUsername": "Имя пользователя не настроено. Пожалуйста, сначала сохраните имя пользователя.",
|
||||||
|
"switchUsernameSuccess": "Успешно переключено на \"{username}\"!",
|
||||||
|
"switchUsernameFailed": "Не удалось переключить имя пользователя",
|
||||||
|
"playerNameTooLong": "Имя игрока должно быть не более 16 символов"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Подтвердить действие",
|
"defaultTitle": "Подтвердить действие",
|
||||||
@@ -226,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Удалить",
|
"deleteUuidButton": "Удалить",
|
||||||
"uninstallGameTitle": "Удалить игру",
|
"uninstallGameTitle": "Удалить игру",
|
||||||
"uninstallGameMessage": "Вы уверены, что хотите удалить Hytale? Все данные игры будут безвозвратно удалены!",
|
"uninstallGameMessage": "Вы уверены, что хотите удалить Hytale? Все данные игры будут безвозвратно удалены!",
|
||||||
"uninstallGameButton": "Удалить"
|
"uninstallGameButton": "Удалить",
|
||||||
|
"switchUsernameTitle": "Сменить личность",
|
||||||
|
"switchUsernameMessage": "Переключиться на имя пользователя \"{username}\"? Это изменит вашу текущую личность игрока.",
|
||||||
|
"switchUsernameButton": "Переключить"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Инициализация...",
|
"initializing": "Инициализация...",
|
||||||
|
|||||||
@@ -211,7 +211,11 @@
|
|||||||
"modsDeleteFailed": "Misslyckades med att ta bort modd: {error}",
|
"modsDeleteFailed": "Misslyckades med att ta bort modd: {error}",
|
||||||
"modsModNotFound": "Moddinformation hittades inte",
|
"modsModNotFound": "Moddinformation hittades inte",
|
||||||
"hwAccelSaved": "Hårdvaruaccelerationsinställning sparad",
|
"hwAccelSaved": "Hårdvaruaccelerationsinställning sparad",
|
||||||
"hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning"
|
"hwAccelSaveFailed": "Misslyckades med att spara hårdvaruaccelerationsinställning",
|
||||||
|
"noUsername": "Inget användarnamn konfigurerat. Vänligen spara ditt användarnamn först.",
|
||||||
|
"switchUsernameSuccess": "Bytte till \"{username}\" framgångsrikt!",
|
||||||
|
"switchUsernameFailed": "Misslyckades med att byta användarnamn",
|
||||||
|
"playerNameTooLong": "Spelarnamnet måste vara 16 tecken eller mindre"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Bekräfta åtgärd",
|
"defaultTitle": "Bekräfta åtgärd",
|
||||||
@@ -226,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Ta bort",
|
"deleteUuidButton": "Ta bort",
|
||||||
"uninstallGameTitle": "Avinstallera spel",
|
"uninstallGameTitle": "Avinstallera spel",
|
||||||
"uninstallGameMessage": "Är du säker på att du vill avinstallera Hytale? Alla spelfiler kommer att tas bort.",
|
"uninstallGameMessage": "Är du säker på att du vill avinstallera Hytale? Alla spelfiler kommer att tas bort.",
|
||||||
"uninstallGameButton": "Avinstallera"
|
"uninstallGameButton": "Avinstallera",
|
||||||
|
"switchUsernameTitle": "Byt identitet",
|
||||||
|
"switchUsernameMessage": "Byta till användarnamn \"{username}\"? Detta kommer att ändra din nuvarande spelaridentitet.",
|
||||||
|
"switchUsernameButton": "Byt"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Initierar...",
|
"initializing": "Initierar...",
|
||||||
|
|||||||
@@ -211,7 +211,11 @@
|
|||||||
"modsDeleteFailed": "Mod silinemedi: {error}",
|
"modsDeleteFailed": "Mod silinemedi: {error}",
|
||||||
"modsModNotFound": "Mod bilgileri bulunamadı",
|
"modsModNotFound": "Mod bilgileri bulunamadı",
|
||||||
"hwAccelSaved": "Donanım hızlandırma ayarı kaydedildi",
|
"hwAccelSaved": "Donanım hızlandırma ayarı kaydedildi",
|
||||||
"hwAccelSaveFailed": "Donanım hızlandırma ayarı kaydedilemedi"
|
"hwAccelSaveFailed": "Donanım hızlandırma ayarı kaydedilemedi",
|
||||||
|
"noUsername": "Kullanıcı adı yapılandırılmadı. Lütfen önce kullanıcı adınızı kaydedin.",
|
||||||
|
"switchUsernameSuccess": "\"{username}\" adına başarıyla geçildi!",
|
||||||
|
"switchUsernameFailed": "Kullanıcı adı değiştirilemedi",
|
||||||
|
"playerNameTooLong": "Oyuncu adı 16 karakter veya daha az olmalıdır"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"defaultTitle": "Eylemi onayla",
|
"defaultTitle": "Eylemi onayla",
|
||||||
@@ -226,7 +230,10 @@
|
|||||||
"deleteUuidButton": "Sil",
|
"deleteUuidButton": "Sil",
|
||||||
"uninstallGameTitle": "Oyunu kaldır",
|
"uninstallGameTitle": "Oyunu kaldır",
|
||||||
"uninstallGameMessage": "Hytale'yi kaldırmak istediğinizden emin misiniz? Tüm oyun dosyaları silinecektir.",
|
"uninstallGameMessage": "Hytale'yi kaldırmak istediğinizden emin misiniz? Tüm oyun dosyaları silinecektir.",
|
||||||
"uninstallGameButton": "Kaldır"
|
"uninstallGameButton": "Kaldır",
|
||||||
|
"switchUsernameTitle": "Kimlik Değiştir",
|
||||||
|
"switchUsernameMessage": "\"{username}\" kullanıcı adına geçilsin mi? Bu mevcut oyuncu kimliğinizi değiştirecektir.",
|
||||||
|
"switchUsernameButton": "Değiştir"
|
||||||
},
|
},
|
||||||
"progress": {
|
"progress": {
|
||||||
"initializing": "Başlatılıyor...",
|
"initializing": "Başlatılıyor...",
|
||||||
|
|||||||
@@ -5236,6 +5236,21 @@ select.settings-input option {
|
|||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
-webkit-user-select: text !important;
|
||||||
|
-moz-user-select: text !important;
|
||||||
|
-ms-user-select: text !important;
|
||||||
|
user-select: text !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure all input fields allow text selection and paste */
|
||||||
|
input[type="text"].uuid-input,
|
||||||
|
#customUuidInput {
|
||||||
|
-webkit-user-select: text !important;
|
||||||
|
-moz-user-select: text !important;
|
||||||
|
-ms-user-select: text !important;
|
||||||
|
user-select: text !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uuid-btn {
|
.uuid-btn {
|
||||||
@@ -5623,6 +5638,12 @@ select.settings-input option {
|
|||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uuid-item-btn.switch:hover {
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
border-color: rgba(59, 130, 246, 0.4);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.uuid-modal-content {
|
.uuid-modal-content {
|
||||||
width: 95vw;
|
width: 95vw;
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// UUID PERSISTENCE FIX - Atomic writes, backups, validation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
// Default auth domain - can be overridden by env var or config
|
// Default auth domain - can be overridden by env var or config
|
||||||
const DEFAULT_AUTH_DOMAIN = 'auth.sanasol.ws';
|
const DEFAULT_AUTH_DOMAIN = 'auth.sanasol.ws';
|
||||||
@@ -49,57 +52,443 @@ function getAppDir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_FILE = path.join(getAppDir(), 'config.json');
|
const CONFIG_FILE = path.join(getAppDir(), 'config.json');
|
||||||
|
const CONFIG_BACKUP = path.join(getAppDir(), 'config.json.bak');
|
||||||
|
const CONFIG_TEMP = path.join(getAppDir(), 'config.json.tmp');
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONFIG VALIDATION
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate config structure - ensures critical data is intact
|
||||||
|
*/
|
||||||
|
function validateConfig(config) {
|
||||||
|
if (!config || typeof config !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If userUuids exists, it must be an object
|
||||||
|
if (config.userUuids !== undefined && typeof config.userUuids !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If username exists, it must be a non-empty string
|
||||||
|
if (config.username !== undefined && (typeof config.username !== 'string')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONFIG LOADING - With backup recovery
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load config with automatic backup recovery
|
||||||
|
* Never returns empty object silently if data existed before
|
||||||
|
*/
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
|
// Try primary config first
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(CONFIG_FILE)) {
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
const data = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||||
|
if (data.trim()) {
|
||||||
|
const config = JSON.parse(data);
|
||||||
|
if (validateConfig(config)) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
console.warn('[Config] Primary config invalid structure, trying backup...');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Notice: could not load config:', err.message);
|
console.error('[Config] Failed to load primary config:', err.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try backup config
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(CONFIG_BACKUP)) {
|
||||||
|
const data = fs.readFileSync(CONFIG_BACKUP, 'utf8');
|
||||||
|
if (data.trim()) {
|
||||||
|
const config = JSON.parse(data);
|
||||||
|
if (validateConfig(config)) {
|
||||||
|
console.log('[Config] Recovered from backup successfully');
|
||||||
|
// Restore primary from backup
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(CONFIG_FILE, data, 'utf8');
|
||||||
|
console.log('[Config] Primary config restored from backup');
|
||||||
|
} catch (restoreErr) {
|
||||||
|
console.error('[Config] Failed to restore primary from backup:', restoreErr.message);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Config] Failed to load backup config:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No valid config - return empty (fresh install)
|
||||||
|
console.log('[Config] No valid config found - fresh install');
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CONFIG SAVING - Atomic writes with backup
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save config atomically with backup
|
||||||
|
* Uses temp file + rename pattern to prevent corruption
|
||||||
|
* Creates backup before overwriting
|
||||||
|
*/
|
||||||
function saveConfig(update) {
|
function saveConfig(update) {
|
||||||
try {
|
const maxRetries = 3;
|
||||||
const configDir = path.dirname(CONFIG_FILE);
|
let lastError;
|
||||||
if (!fs.existsSync(configDir)) {
|
|
||||||
fs.mkdirSync(configDir, { recursive: true });
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const configDir = path.dirname(CONFIG_FILE);
|
||||||
|
if (!fs.existsSync(configDir)) {
|
||||||
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load current config
|
||||||
|
const currentConfig = loadConfig();
|
||||||
|
const newConfig = { ...currentConfig, ...update };
|
||||||
|
const data = JSON.stringify(newConfig, null, 2);
|
||||||
|
|
||||||
|
// 1. Write to temp file first
|
||||||
|
fs.writeFileSync(CONFIG_TEMP, data, 'utf8');
|
||||||
|
|
||||||
|
// 2. Verify temp file is valid JSON
|
||||||
|
const verification = JSON.parse(fs.readFileSync(CONFIG_TEMP, 'utf8'));
|
||||||
|
if (!validateConfig(verification)) {
|
||||||
|
throw new Error('Config validation failed after write');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Backup current config (if exists and valid)
|
||||||
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
|
try {
|
||||||
|
const currentData = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||||
|
if (currentData.trim()) {
|
||||||
|
fs.writeFileSync(CONFIG_BACKUP, currentData, 'utf8');
|
||||||
|
}
|
||||||
|
} catch (backupErr) {
|
||||||
|
console.warn('[Config] Could not create backup:', backupErr.message);
|
||||||
|
// Continue anyway - saving new config is more important
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Atomic rename (this is the critical operation)
|
||||||
|
fs.renameSync(CONFIG_TEMP, CONFIG_FILE);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
lastError = err;
|
||||||
|
console.error(`[Config] Save attempt ${attempt}/${maxRetries} failed:`, err.message);
|
||||||
|
|
||||||
|
// Clean up temp file on failure
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(CONFIG_TEMP)) {
|
||||||
|
fs.unlinkSync(CONFIG_TEMP);
|
||||||
|
}
|
||||||
|
} catch (cleanupErr) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
// Small delay before retry
|
||||||
|
const delay = attempt * 100;
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < delay) {
|
||||||
|
// Busy wait (sync delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const config = loadConfig();
|
|
||||||
const next = { ...config, ...update };
|
|
||||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf8');
|
|
||||||
} catch (err) {
|
|
||||||
console.log('Notice: could not save config:', err.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All retries failed - this is critical
|
||||||
|
console.error('[Config] CRITICAL: Failed to save config after all retries:', lastError.message);
|
||||||
|
throw new Error(`Failed to save config: ${lastError.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// USERNAME MANAGEMENT - No silent fallbacks
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save username to config
|
||||||
|
* When changing username, the UUID is preserved (rename, not new identity)
|
||||||
|
* Validates username before saving
|
||||||
|
*/
|
||||||
function saveUsername(username) {
|
function saveUsername(username) {
|
||||||
saveConfig({ username: username || 'Player' });
|
if (!username || typeof username !== 'string') {
|
||||||
|
throw new Error('Invalid username: must be a non-empty string');
|
||||||
|
}
|
||||||
|
const newName = username.trim();
|
||||||
|
if (!newName) {
|
||||||
|
throw new Error('Invalid username: cannot be empty or whitespace');
|
||||||
|
}
|
||||||
|
if (newName.length > 16) {
|
||||||
|
throw new Error('Invalid username: must be 16 characters or less');
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
const currentName = config.username ? config.username.trim() : null;
|
||||||
|
const userUuids = config.userUuids || {};
|
||||||
|
|
||||||
|
// Check if we're actually changing the username (case-insensitive comparison)
|
||||||
|
const isRename = currentName && currentName.toLowerCase() !== newName.toLowerCase();
|
||||||
|
|
||||||
|
if (isRename) {
|
||||||
|
// Find the UUID for the current username
|
||||||
|
const currentKey = Object.keys(userUuids).find(
|
||||||
|
k => k.toLowerCase() === currentName.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentKey && userUuids[currentKey]) {
|
||||||
|
// Check if target username already exists (would be a different identity)
|
||||||
|
const targetKey = Object.keys(userUuids).find(
|
||||||
|
k => k.toLowerCase() === newName.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetKey) {
|
||||||
|
// Target username already exists - this is switching identity, not renaming
|
||||||
|
console.log(`[Config] Switching to existing identity: "${newName}" (UUID already exists)`);
|
||||||
|
} else {
|
||||||
|
// Rename: move UUID from old name to new name
|
||||||
|
const uuid = userUuids[currentKey];
|
||||||
|
delete userUuids[currentKey];
|
||||||
|
userUuids[newName] = uuid;
|
||||||
|
console.log(`[Config] Renamed identity: "${currentKey}" → "${newName}" (UUID preserved: ${uuid})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (currentName && currentName !== newName) {
|
||||||
|
// Case change only - update the key to preserve the new casing
|
||||||
|
const currentKey = Object.keys(userUuids).find(
|
||||||
|
k => k.toLowerCase() === currentName.toLowerCase()
|
||||||
|
);
|
||||||
|
if (currentKey && currentKey !== newName) {
|
||||||
|
const uuid = userUuids[currentKey];
|
||||||
|
delete userUuids[currentKey];
|
||||||
|
userUuids[newName] = uuid;
|
||||||
|
console.log(`[Config] Updated username case: "${currentKey}" → "${newName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save both username and updated userUuids
|
||||||
|
saveConfig({ username: newName, userUuids });
|
||||||
|
console.log(`[Config] Username saved: "${newName}"`);
|
||||||
|
return newName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load username from config
|
||||||
|
* Returns null if no username set (caller must handle)
|
||||||
|
*/
|
||||||
function loadUsername() {
|
function loadUsername() {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
return config.username || 'Player';
|
const username = config.username;
|
||||||
|
if (username && typeof username === 'string' && username.trim()) {
|
||||||
|
return username.trim();
|
||||||
|
}
|
||||||
|
return null; // No username set - caller must handle this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load username with fallback to 'Player'
|
||||||
|
* Use this only for display purposes, NOT for UUID lookup
|
||||||
|
*/
|
||||||
|
function loadUsernameWithDefault() {
|
||||||
|
return loadUsername() || 'Player';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if username is configured
|
||||||
|
*/
|
||||||
|
function hasUsername() {
|
||||||
|
return loadUsername() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// UUID MANAGEMENT - Persistent and safe
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize username for UUID lookup (case-insensitive, trimmed)
|
||||||
|
*/
|
||||||
|
function normalizeUsername(username) {
|
||||||
|
if (!username || typeof username !== 'string') return null;
|
||||||
|
return username.trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get UUID for a username
|
||||||
|
* Creates new UUID only if user explicitly doesn't exist
|
||||||
|
* Uses case-insensitive lookup to prevent duplicates, but preserves original case for display
|
||||||
|
*/
|
||||||
function getUuidForUser(username) {
|
function getUuidForUser(username) {
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
|
if (!username || typeof username !== 'string' || !username.trim()) {
|
||||||
|
throw new Error('Cannot get UUID: username is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayName = username.trim();
|
||||||
|
const normalizedLookup = displayName.toLowerCase();
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const userUuids = config.userUuids || {};
|
const userUuids = config.userUuids || {};
|
||||||
|
|
||||||
if (userUuids[username]) {
|
// Case-insensitive lookup - find existing key regardless of case
|
||||||
return userUuids[username];
|
const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
||||||
|
|
||||||
|
if (existingKey) {
|
||||||
|
// Found existing - return UUID, update display name if case changed
|
||||||
|
const existingUuid = userUuids[existingKey];
|
||||||
|
|
||||||
|
// If user typed different case, update the key to new case (preserving UUID)
|
||||||
|
if (existingKey !== displayName) {
|
||||||
|
console.log(`[Config] Updating username case: "${existingKey}" → "${displayName}"`);
|
||||||
|
delete userUuids[existingKey];
|
||||||
|
userUuids[displayName] = existingUuid;
|
||||||
|
saveConfig({ userUuids });
|
||||||
|
}
|
||||||
|
|
||||||
|
return existingUuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create new UUID for new user - store with original case
|
||||||
const newUuid = uuidv4();
|
const newUuid = uuidv4();
|
||||||
userUuids[username] = newUuid;
|
userUuids[displayName] = newUuid;
|
||||||
saveConfig({ userUuids });
|
saveConfig({ userUuids });
|
||||||
|
console.log(`[Config] Created new UUID for "${displayName}": ${newUuid}`);
|
||||||
|
|
||||||
return newUuid;
|
return newUuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current user's UUID (based on saved username)
|
||||||
|
*/
|
||||||
|
function getCurrentUuid() {
|
||||||
|
const username = loadUsername();
|
||||||
|
if (!username) {
|
||||||
|
throw new Error('Cannot get current UUID: no username configured');
|
||||||
|
}
|
||||||
|
return getUuidForUser(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all UUID mappings (raw object)
|
||||||
|
*/
|
||||||
|
function getAllUuidMappings() {
|
||||||
|
const config = loadConfig();
|
||||||
|
return config.userUuids || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all UUID mappings as array with current user flag
|
||||||
|
*/
|
||||||
|
function getAllUuidMappingsArray() {
|
||||||
|
const config = loadConfig();
|
||||||
|
const userUuids = config.userUuids || {};
|
||||||
|
const currentUsername = loadUsername();
|
||||||
|
// Case-insensitive comparison for isCurrent
|
||||||
|
const normalizedCurrent = currentUsername ? currentUsername.toLowerCase() : null;
|
||||||
|
|
||||||
|
return Object.entries(userUuids).map(([username, uuid]) => ({
|
||||||
|
username, // Original case preserved
|
||||||
|
uuid,
|
||||||
|
isCurrent: username.toLowerCase() === normalizedCurrent
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set UUID for a specific user
|
||||||
|
* Validates UUID format before saving
|
||||||
|
* Preserves original case of username
|
||||||
|
*/
|
||||||
|
function setUuidForUser(username, uuid) {
|
||||||
|
const { validate: validateUuid } = require('uuid');
|
||||||
|
|
||||||
|
if (!username || typeof username !== 'string' || !username.trim()) {
|
||||||
|
throw new Error('Invalid username');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateUuid(uuid)) {
|
||||||
|
throw new Error('Invalid UUID format');
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayName = username.trim();
|
||||||
|
const normalizedLookup = displayName.toLowerCase();
|
||||||
|
const config = loadConfig();
|
||||||
|
const userUuids = config.userUuids || {};
|
||||||
|
|
||||||
|
// Remove any existing entry with same name (case-insensitive)
|
||||||
|
const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
||||||
|
if (existingKey) {
|
||||||
|
delete userUuids[existingKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store with original case
|
||||||
|
userUuids[displayName] = uuid;
|
||||||
|
saveConfig({ userUuids });
|
||||||
|
|
||||||
|
console.log(`[Config] UUID set for "${displayName}": ${uuid}`);
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new UUID (without saving)
|
||||||
|
*/
|
||||||
|
function generateNewUuid() {
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
return uuidv4();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete UUID for a specific user
|
||||||
|
* Uses case-insensitive lookup
|
||||||
|
*/
|
||||||
|
function deleteUuidForUser(username) {
|
||||||
|
if (!username || typeof username !== 'string') {
|
||||||
|
throw new Error('Invalid username');
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedLookup = username.trim().toLowerCase();
|
||||||
|
const config = loadConfig();
|
||||||
|
const userUuids = config.userUuids || {};
|
||||||
|
|
||||||
|
// Case-insensitive lookup
|
||||||
|
const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
||||||
|
|
||||||
|
if (existingKey) {
|
||||||
|
delete userUuids[existingKey];
|
||||||
|
saveConfig({ userUuids });
|
||||||
|
console.log(`[Config] UUID deleted for "${username}"`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset current user's UUID (generates new one)
|
||||||
|
*/
|
||||||
|
function resetCurrentUserUuid() {
|
||||||
|
const username = loadUsername();
|
||||||
|
if (!username) {
|
||||||
|
throw new Error('Cannot reset UUID: no username configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
const newUuid = uuidv4();
|
||||||
|
|
||||||
|
return setUuidForUser(username, newUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// JAVA PATH MANAGEMENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveJavaPath(javaPath) {
|
function saveJavaPath(javaPath) {
|
||||||
const trimmed = (javaPath || '').trim();
|
const trimmed = (javaPath || '').trim();
|
||||||
saveConfig({ javaPath: trimmed });
|
saveConfig({ javaPath: trimmed });
|
||||||
@@ -120,6 +509,10 @@ function loadJavaPath() {
|
|||||||
return config.javaPath || '';
|
return config.javaPath || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// INSTALL PATH MANAGEMENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveInstallPath(installPath) {
|
function saveInstallPath(installPath) {
|
||||||
const trimmed = (installPath || '').trim();
|
const trimmed = (installPath || '').trim();
|
||||||
saveConfig({ installPath: trimmed });
|
saveConfig({ installPath: trimmed });
|
||||||
@@ -130,6 +523,10 @@ function loadInstallPath() {
|
|||||||
return config.installPath || '';
|
return config.installPath || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// DISCORD RPC SETTINGS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveDiscordRPC(enabled) {
|
function saveDiscordRPC(enabled) {
|
||||||
saveConfig({ discordRPC: !!enabled });
|
saveConfig({ discordRPC: !!enabled });
|
||||||
}
|
}
|
||||||
@@ -139,6 +536,10 @@ function loadDiscordRPC() {
|
|||||||
return config.discordRPC !== undefined ? config.discordRPC : true;
|
return config.discordRPC !== undefined ? config.discordRPC : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// LANGUAGE SETTINGS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveLanguage(language) {
|
function saveLanguage(language) {
|
||||||
saveConfig({ language: language || 'en' });
|
saveConfig({ language: language || 'en' });
|
||||||
}
|
}
|
||||||
@@ -148,6 +549,10 @@ function loadLanguage() {
|
|||||||
return config.language || 'en';
|
return config.language || 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// LAUNCHER SETTINGS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveCloseLauncherOnStart(enabled) {
|
function saveCloseLauncherOnStart(enabled) {
|
||||||
saveConfig({ closeLauncherOnStart: !!enabled });
|
saveConfig({ closeLauncherOnStart: !!enabled });
|
||||||
}
|
}
|
||||||
@@ -166,31 +571,38 @@ function loadLauncherHardwareAcceleration() {
|
|||||||
return config.launcherHardwareAcceleration !== undefined ? config.launcherHardwareAcceleration : true;
|
return config.launcherHardwareAcceleration !== undefined ? config.launcherHardwareAcceleration : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// MODS MANAGEMENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveModsToConfig(mods) {
|
function saveModsToConfig(mods) {
|
||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
// Config migration handles structure, but mod saves must go to the ACTIVE profile.
|
|
||||||
// Global installedMods is kept mainly for reference/migration.
|
|
||||||
// The profile is the source of truth for enabled mods.
|
|
||||||
|
|
||||||
|
|
||||||
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
||||||
config.profiles[config.activeProfileId].mods = mods;
|
config.profiles[config.activeProfileId].mods = mods;
|
||||||
} else {
|
} else {
|
||||||
// Fallback for legacy or no-profile state
|
|
||||||
config.installedMods = mods;
|
config.installedMods = mods;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use atomic save
|
||||||
const configDir = path.dirname(CONFIG_FILE);
|
const configDir = path.dirname(CONFIG_FILE);
|
||||||
if (!fs.existsSync(configDir)) {
|
if (!fs.existsSync(configDir)) {
|
||||||
fs.mkdirSync(configDir, { recursive: true });
|
fs.mkdirSync(configDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
// Write atomically
|
||||||
console.log('Mods saved to config.json');
|
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);
|
||||||
|
|
||||||
|
console.log('[Config] Mods saved successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving mods to config:', error);
|
console.error('[Config] Error saving mods:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,25 +610,34 @@ function loadModsFromConfig() {
|
|||||||
try {
|
try {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
// Prefer Active Profile
|
|
||||||
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
||||||
return config.profiles[config.activeProfileId].mods || [];
|
return config.profiles[config.activeProfileId].mods || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return config.installedMods || [];
|
return config.installedMods || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading mods from config:', error);
|
console.error('[Config] Error loading mods:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// FIRST LAUNCH DETECTION - FIXED
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this is the first launch
|
||||||
|
* FIXED: Was always returning true due to bug
|
||||||
|
*/
|
||||||
function isFirstLaunch() {
|
function isFirstLaunch() {
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
|
// If explicitly marked, use that
|
||||||
if ('hasLaunchedBefore' in config) {
|
if ('hasLaunchedBefore' in config) {
|
||||||
return !config.hasLaunchedBefore;
|
return !config.hasLaunchedBefore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for any existing user data
|
||||||
const hasUserData = config.installPath || config.username || config.javaPath ||
|
const hasUserData = config.installPath || config.username || config.javaPath ||
|
||||||
config.chatUsername || config.userUuids ||
|
config.chatUsername || config.userUuids ||
|
||||||
Object.keys(config).length > 0;
|
Object.keys(config).length > 0;
|
||||||
@@ -225,65 +646,17 @@ function isFirstLaunch() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// FIXED: Was returning true here, should be false
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function markAsLaunched() {
|
function markAsLaunched() {
|
||||||
saveConfig({ hasLaunchedBefore: true, firstLaunchDate: new Date().toISOString() });
|
saveConfig({ hasLaunchedBefore: true, firstLaunchDate: new Date().toISOString() });
|
||||||
}
|
}
|
||||||
|
|
||||||
// UUID Management Functions
|
// =============================================================================
|
||||||
function getCurrentUuid() {
|
// GPU PREFERENCE
|
||||||
const username = loadUsername();
|
// =============================================================================
|
||||||
return getUuidForUser(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAllUuidMappings() {
|
|
||||||
const config = loadConfig();
|
|
||||||
return config.userUuids || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setUuidForUser(username, uuid) {
|
|
||||||
const { v4: uuidv4, validate: validateUuid } = require('uuid');
|
|
||||||
|
|
||||||
// Validate UUID format
|
|
||||||
if (!validateUuid(uuid)) {
|
|
||||||
throw new Error('Invalid UUID format');
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
const userUuids = config.userUuids || {};
|
|
||||||
userUuids[username] = uuid;
|
|
||||||
saveConfig({ userUuids });
|
|
||||||
|
|
||||||
return uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateNewUuid() {
|
|
||||||
const { v4: uuidv4 } = require('uuid');
|
|
||||||
return uuidv4();
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteUuidForUser(username) {
|
|
||||||
const config = loadConfig();
|
|
||||||
const userUuids = config.userUuids || {};
|
|
||||||
|
|
||||||
if (userUuids[username]) {
|
|
||||||
delete userUuids[username];
|
|
||||||
saveConfig({ userUuids });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetCurrentUserUuid() {
|
|
||||||
const username = loadUsername();
|
|
||||||
const { v4: uuidv4 } = require('uuid');
|
|
||||||
const newUuid = uuidv4();
|
|
||||||
|
|
||||||
return setUuidForUser(username, newUuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveGpuPreference(gpuPreference) {
|
function saveGpuPreference(gpuPreference) {
|
||||||
saveConfig({ gpuPreference: gpuPreference || 'auto' });
|
saveConfig({ gpuPreference: gpuPreference || 'auto' });
|
||||||
@@ -294,6 +667,10 @@ function loadGpuPreference() {
|
|||||||
return config.gpuPreference || 'auto';
|
return config.gpuPreference || 'auto';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// VERSION MANAGEMENT
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
function saveVersionClient(versionClient) {
|
function saveVersionClient(versionClient) {
|
||||||
saveConfig({ version_client: versionClient });
|
saveConfig({ version_client: versionClient });
|
||||||
}
|
}
|
||||||
@@ -306,7 +683,7 @@ function loadVersionClient() {
|
|||||||
function saveVersionBranch(versionBranch) {
|
function saveVersionBranch(versionBranch) {
|
||||||
const branch = versionBranch || 'release';
|
const branch = versionBranch || 'release';
|
||||||
if (branch !== 'release' && branch !== 'pre-release') {
|
if (branch !== 'release' && branch !== 'pre-release') {
|
||||||
console.warn(`Invalid branch "${branch}", defaulting to "release"`);
|
console.warn(`[Config] Invalid branch "${branch}", defaulting to "release"`);
|
||||||
saveConfig({ version_branch: 'release' });
|
saveConfig({ version_branch: 'release' });
|
||||||
} else {
|
} else {
|
||||||
saveConfig({ version_branch: branch });
|
saveConfig({ version_branch: branch });
|
||||||
@@ -318,50 +695,98 @@ function loadVersionBranch() {
|
|||||||
return config.version_branch || 'release';
|
return config.version_branch || 'release';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// READY STATE - For UI to check before allowing launch
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if launcher is ready to launch game
|
||||||
|
* Returns object with ready state and any issues
|
||||||
|
*/
|
||||||
|
function checkLaunchReady() {
|
||||||
|
const issues = [];
|
||||||
|
|
||||||
|
const username = loadUsername();
|
||||||
|
if (!username) {
|
||||||
|
issues.push('No username configured');
|
||||||
|
} else if (username === 'Player') {
|
||||||
|
issues.push('Using default username "Player"');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ready: issues.length === 0,
|
||||||
|
hasUsername: !!username,
|
||||||
|
username: username,
|
||||||
|
issues: issues
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// EXPORTS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
// Core config
|
||||||
loadConfig,
|
loadConfig,
|
||||||
saveConfig,
|
saveConfig,
|
||||||
|
validateConfig,
|
||||||
|
|
||||||
|
// Username (no silent fallbacks)
|
||||||
saveUsername,
|
saveUsername,
|
||||||
loadUsername,
|
loadUsername,
|
||||||
|
loadUsernameWithDefault,
|
||||||
|
hasUsername,
|
||||||
|
|
||||||
|
// UUID management
|
||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
saveJavaPath,
|
|
||||||
loadJavaPath,
|
|
||||||
saveInstallPath,
|
|
||||||
loadInstallPath,
|
|
||||||
saveDiscordRPC,
|
|
||||||
loadDiscordRPC,
|
|
||||||
saveLanguage,
|
|
||||||
loadLanguage,
|
|
||||||
saveModsToConfig,
|
|
||||||
loadModsFromConfig,
|
|
||||||
isFirstLaunch,
|
|
||||||
markAsLaunched,
|
|
||||||
CONFIG_FILE,
|
|
||||||
// Auth server exports
|
|
||||||
getAuthServerUrl,
|
|
||||||
getAuthDomain,
|
|
||||||
saveAuthDomain,
|
|
||||||
// UUID Management exports
|
|
||||||
getCurrentUuid,
|
getCurrentUuid,
|
||||||
getAllUuidMappings,
|
getAllUuidMappings,
|
||||||
|
getAllUuidMappingsArray,
|
||||||
setUuidForUser,
|
setUuidForUser,
|
||||||
generateNewUuid,
|
generateNewUuid,
|
||||||
deleteUuidForUser,
|
deleteUuidForUser,
|
||||||
resetCurrentUserUuid,
|
resetCurrentUserUuid,
|
||||||
// GPU Preference exports
|
|
||||||
saveGpuPreference,
|
// Java/Install paths
|
||||||
loadGpuPreference,
|
saveJavaPath,
|
||||||
// Close Launcher export
|
loadJavaPath,
|
||||||
|
saveInstallPath,
|
||||||
|
loadInstallPath,
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
saveDiscordRPC,
|
||||||
|
loadDiscordRPC,
|
||||||
|
saveLanguage,
|
||||||
|
loadLanguage,
|
||||||
saveCloseLauncherOnStart,
|
saveCloseLauncherOnStart,
|
||||||
loadCloseLauncherOnStart,
|
loadCloseLauncherOnStart,
|
||||||
|
|
||||||
// Hardware Acceleration functions
|
|
||||||
saveLauncherHardwareAcceleration,
|
saveLauncherHardwareAcceleration,
|
||||||
loadLauncherHardwareAcceleration,
|
loadLauncherHardwareAcceleration,
|
||||||
|
|
||||||
// Version Management exports
|
// Mods
|
||||||
|
saveModsToConfig,
|
||||||
|
loadModsFromConfig,
|
||||||
|
|
||||||
|
// Launch state
|
||||||
|
isFirstLaunch,
|
||||||
|
markAsLaunched,
|
||||||
|
checkLaunchReady,
|
||||||
|
|
||||||
|
// Auth server
|
||||||
|
getAuthServerUrl,
|
||||||
|
getAuthDomain,
|
||||||
|
saveAuthDomain,
|
||||||
|
|
||||||
|
// GPU
|
||||||
|
saveGpuPreference,
|
||||||
|
loadGpuPreference,
|
||||||
|
|
||||||
|
// Version
|
||||||
saveVersionClient,
|
saveVersionClient,
|
||||||
loadVersionClient,
|
loadVersionClient,
|
||||||
saveVersionBranch,
|
saveVersionBranch,
|
||||||
loadVersionBranch
|
loadVersionBranch,
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
CONFIG_FILE
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
const {
|
const {
|
||||||
saveUsername,
|
saveUsername,
|
||||||
loadUsername,
|
loadUsername,
|
||||||
|
loadUsernameWithDefault,
|
||||||
|
hasUsername,
|
||||||
saveJavaPath,
|
saveJavaPath,
|
||||||
loadJavaPath,
|
loadJavaPath,
|
||||||
saveInstallPath,
|
saveInstallPath,
|
||||||
@@ -27,9 +29,11 @@ const {
|
|||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
isFirstLaunch,
|
isFirstLaunch,
|
||||||
markAsLaunched,
|
markAsLaunched,
|
||||||
|
checkLaunchReady,
|
||||||
// UUID Management
|
// UUID Management
|
||||||
getCurrentUuid,
|
getCurrentUuid,
|
||||||
getAllUuidMappings,
|
getAllUuidMappings,
|
||||||
|
getAllUuidMappingsArray,
|
||||||
setUuidForUser,
|
setUuidForUser,
|
||||||
generateNewUuid,
|
generateNewUuid,
|
||||||
deleteUuidForUser,
|
deleteUuidForUser,
|
||||||
@@ -110,7 +114,10 @@ module.exports = {
|
|||||||
// User configuration functions
|
// User configuration functions
|
||||||
saveUsername,
|
saveUsername,
|
||||||
loadUsername,
|
loadUsername,
|
||||||
|
loadUsernameWithDefault,
|
||||||
|
hasUsername,
|
||||||
getUuidForUser,
|
getUuidForUser,
|
||||||
|
checkLaunchReady,
|
||||||
|
|
||||||
// Java configuration functions
|
// Java configuration functions
|
||||||
saveJavaPath,
|
saveJavaPath,
|
||||||
@@ -162,6 +169,7 @@ module.exports = {
|
|||||||
// UUID Management functions
|
// UUID Management functions
|
||||||
getCurrentUuid,
|
getCurrentUuid,
|
||||||
getAllUuidMappings,
|
getAllUuidMappings,
|
||||||
|
getAllUuidMappingsArray,
|
||||||
setUuidForUser,
|
setUuidForUser,
|
||||||
generateNewUuid,
|
generateNewUuid,
|
||||||
deleteUuidForUser,
|
deleteUuidForUser,
|
||||||
|
|||||||
@@ -7,7 +7,19 @@ const { spawn } = require('child_process');
|
|||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const { getResolvedAppDir, findClientPath } = require('../core/paths');
|
const { getResolvedAppDir, findClientPath } = require('../core/paths');
|
||||||
const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platformUtils');
|
const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platformUtils');
|
||||||
const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain, loadVersionBranch, loadVersionClient, saveVersionClient } = require('../core/config');
|
const {
|
||||||
|
saveInstallPath,
|
||||||
|
loadJavaPath,
|
||||||
|
getUuidForUser,
|
||||||
|
getAuthServerUrl,
|
||||||
|
getAuthDomain,
|
||||||
|
loadVersionBranch,
|
||||||
|
loadVersionClient,
|
||||||
|
saveVersionClient,
|
||||||
|
loadUsername,
|
||||||
|
hasUsername,
|
||||||
|
checkLaunchReady
|
||||||
|
} = require('../core/config');
|
||||||
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
|
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
|
||||||
const { getLatestClientVersion } = require('../services/versionManager');
|
const { getLatestClientVersion } = require('../services/versionManager');
|
||||||
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
|
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
|
||||||
@@ -104,8 +116,42 @@ function generateLocalTokens(uuid, name) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
async function launchGame(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||||
// Synchronize server list on every game launch
|
// ==========================================================================
|
||||||
|
// STEP 1: Validate player identity FIRST (before any other operations)
|
||||||
|
// ==========================================================================
|
||||||
|
const launchState = checkLaunchReady();
|
||||||
|
|
||||||
|
// Load username from config - single source of truth
|
||||||
|
let playerName = loadUsername();
|
||||||
|
|
||||||
|
if (!playerName) {
|
||||||
|
// No username configured - this is a critical error
|
||||||
|
const error = new Error('No username configured. Please set your username in Settings before playing.');
|
||||||
|
console.error('[Launcher] Launch blocked:', error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow override only if explicitly provided (for testing/migration)
|
||||||
|
if (playerNameOverride && typeof playerNameOverride === 'string' && playerNameOverride.trim()) {
|
||||||
|
const overrideName = playerNameOverride.trim();
|
||||||
|
if (overrideName !== playerName && overrideName !== 'Player') {
|
||||||
|
console.warn(`[Launcher] Username override requested: "${overrideName}" (saved: "${playerName}")`);
|
||||||
|
// Use override for this session but DON'T save it - config is source of truth
|
||||||
|
playerName = overrideName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if using default 'Player' name (likely misconfiguration)
|
||||||
|
if (playerName === 'Player') {
|
||||||
|
console.warn('[Launcher] Warning: Using default username "Player". This may cause cosmetic issues.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Launcher] Launching game for player: "${playerName}"`);
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// STEP 2: Synchronize server list
|
||||||
|
// ==========================================================================
|
||||||
try {
|
try {
|
||||||
console.log('[Launcher] Synchronizing server list...');
|
console.log('[Launcher] Synchronizing server list...');
|
||||||
await syncServerList();
|
await syncServerList();
|
||||||
@@ -113,11 +159,14 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
|||||||
console.warn('[Launcher] Server list sync failed, continuing launch:', syncError.message);
|
console.warn('[Launcher] Server list sync failed, continuing launch:', syncError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================================
|
||||||
|
// STEP 3: Setup paths and directories
|
||||||
|
// ==========================================================================
|
||||||
const branch = branchOverride || loadVersionBranch();
|
const branch = branchOverride || loadVersionBranch();
|
||||||
const customAppDir = getResolvedAppDir(installPathOverride);
|
const customAppDir = getResolvedAppDir(installPathOverride);
|
||||||
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||||
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
||||||
|
|
||||||
// NEW 2.2.0: Use centralized UserData location
|
// NEW 2.2.0: Use centralized UserData location
|
||||||
const userDataDir = getUserDataPath();
|
const userDataDir = getUserDataPath();
|
||||||
|
|
||||||
@@ -128,7 +177,10 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
|||||||
throw new Error('Game is not installed. Please install the game first.');
|
throw new Error('Game is not installed. Please install the game first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
saveUsername(playerName);
|
// NOTE: We do NOT save username here anymore - username is only saved
|
||||||
|
// when user explicitly changes it in Settings. This prevents accidental
|
||||||
|
// overwrites from race conditions or default values.
|
||||||
|
|
||||||
if (installPathOverride) {
|
if (installPathOverride) {
|
||||||
saveInstallPath(installPathOverride);
|
saveInstallPath(installPathOverride);
|
||||||
}
|
}
|
||||||
@@ -417,10 +469,26 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
async function launchGameWithVersionCheck(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||||
try {
|
try {
|
||||||
|
// ==========================================================================
|
||||||
|
// PRE-LAUNCH VALIDATION: Check username is configured
|
||||||
|
// ==========================================================================
|
||||||
|
const launchState = checkLaunchReady();
|
||||||
|
|
||||||
|
if (!launchState.hasUsername) {
|
||||||
|
const error = 'No username configured. Please set your username in Settings before playing.';
|
||||||
|
console.error('[Launcher] Launch blocked:', error);
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback(error, -1, null, null, null);
|
||||||
|
}
|
||||||
|
return { success: false, error: error, needsUsername: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Launcher] Pre-launch check passed. Username: "${launchState.username}"`);
|
||||||
|
|
||||||
const branch = branchOverride || loadVersionBranch();
|
const branch = branchOverride || loadVersionBranch();
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Checking for updates...', 0, null, null, null);
|
progressCallback('Checking for updates...', 0, null, null, null);
|
||||||
}
|
}
|
||||||
@@ -474,7 +542,7 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
|
|||||||
progressCallback('Launching game...', 80, null, null, null);
|
progressCallback('Launching game...', 80, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const launchResult = await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
|
const launchResult = await launchGame(playerNameOverride, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
|
||||||
|
|
||||||
// Ensure we always return a result
|
// Ensure we always return a result
|
||||||
if (!launchResult) {
|
if (!launchResult) {
|
||||||
|
|||||||
@@ -472,7 +472,9 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
saveUsername(playerName);
|
// NOTE: Do NOT save username here - username should only be saved when user explicitly
|
||||||
|
// changes it in Settings. Saving here could overwrite a good username with 'Player' default.
|
||||||
|
// The username is only needed for launching, not for installing.
|
||||||
if (installPathOverride) {
|
if (installPathOverride) {
|
||||||
saveInstallPath(installPathOverride);
|
saveInstallPath(installPathOverride);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,32 +3,117 @@ const path = require('path');
|
|||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
const { PLAYER_ID_FILE, APP_DIR } = require('../core/paths');
|
const { PLAYER_ID_FILE, APP_DIR } = require('../core/paths');
|
||||||
|
|
||||||
function getOrCreatePlayerId() {
|
/**
|
||||||
try {
|
* DEPRECATED: This file is kept for backward compatibility.
|
||||||
if (!fs.existsSync(APP_DIR)) {
|
*
|
||||||
fs.mkdirSync(APP_DIR, { recursive: true });
|
* The primary UUID system is now in config.js using userUuids.
|
||||||
}
|
* This player_id.json system was a separate UUID storage that could
|
||||||
|
* cause desync issues.
|
||||||
|
*
|
||||||
|
* New code should use config.js functions:
|
||||||
|
* - getUuidForUser(username) - Get/create UUID for a username
|
||||||
|
* - getCurrentUuid() - Get current user's UUID
|
||||||
|
* - setUuidForUser(username, uuid) - Set UUID for a user
|
||||||
|
*
|
||||||
|
* This function is kept for migration purposes only.
|
||||||
|
*/
|
||||||
|
|
||||||
if (fs.existsSync(PLAYER_ID_FILE)) {
|
/**
|
||||||
const data = JSON.parse(fs.readFileSync(PLAYER_ID_FILE, 'utf8'));
|
* Get or create a legacy player ID
|
||||||
if (data.playerId) {
|
* NOTE: This is DEPRECATED - use config.js getUuidForUser() instead
|
||||||
return data.playerId;
|
*
|
||||||
|
* FIXED: No longer returns random UUID on error - throws instead
|
||||||
|
*/
|
||||||
|
function getOrCreatePlayerId() {
|
||||||
|
const maxRetries = 3;
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(APP_DIR)) {
|
||||||
|
fs.mkdirSync(APP_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(PLAYER_ID_FILE)) {
|
||||||
|
const data = fs.readFileSync(PLAYER_ID_FILE, 'utf8');
|
||||||
|
if (data.trim()) {
|
||||||
|
const parsed = JSON.parse(data);
|
||||||
|
if (parsed.playerId) {
|
||||||
|
return parsed.playerId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No existing ID - create new one atomically
|
||||||
|
const newPlayerId = uuidv4();
|
||||||
|
const tempFile = PLAYER_ID_FILE + '.tmp';
|
||||||
|
const playerData = {
|
||||||
|
playerId: newPlayerId,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
note: 'DEPRECATED: This file is for legacy compatibility. UUID is now stored in config.json userUuids.'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write to temp file first
|
||||||
|
fs.writeFileSync(tempFile, JSON.stringify(playerData, null, 2));
|
||||||
|
|
||||||
|
// Atomic rename
|
||||||
|
fs.renameSync(tempFile, PLAYER_ID_FILE);
|
||||||
|
|
||||||
|
console.log(`[PlayerManager] Created new legacy player ID: ${newPlayerId}`);
|
||||||
|
return newPlayerId;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
console.error(`[PlayerManager] Attempt ${attempt}/${maxRetries} failed:`, error.message);
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
// Small delay before retry
|
||||||
|
const delay = attempt * 100;
|
||||||
|
const start = Date.now();
|
||||||
|
while (Date.now() - start < delay) {
|
||||||
|
// Busy wait
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newPlayerId = uuidv4();
|
// FIXED: Do NOT return random UUID - throw error instead
|
||||||
fs.writeFileSync(PLAYER_ID_FILE, JSON.stringify({
|
// Returning random UUID was causing silent identity loss
|
||||||
playerId: newPlayerId,
|
console.error('[PlayerManager] CRITICAL: Failed to get/create player ID after all retries');
|
||||||
createdAt: new Date().toISOString()
|
throw new Error(`Failed to manage player ID: ${lastError.message}`);
|
||||||
}, null, 2));
|
}
|
||||||
|
|
||||||
return newPlayerId;
|
/**
|
||||||
|
* Migrate legacy player_id.json to config.json userUuids
|
||||||
|
* Call this during app startup
|
||||||
|
*/
|
||||||
|
function migrateLegacyPlayerId() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(PLAYER_ID_FILE)) {
|
||||||
|
return null; // No legacy file to migrate
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = JSON.parse(fs.readFileSync(PLAYER_ID_FILE, 'utf8'));
|
||||||
|
if (!data.playerId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[PlayerManager] Found legacy player_id.json with ID: ${data.playerId}`);
|
||||||
|
|
||||||
|
// Mark file as migrated by renaming
|
||||||
|
const migratedFile = PLAYER_ID_FILE + '.migrated';
|
||||||
|
if (!fs.existsSync(migratedFile)) {
|
||||||
|
fs.renameSync(PLAYER_ID_FILE, migratedFile);
|
||||||
|
console.log('[PlayerManager] Legacy player_id.json marked as migrated');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.playerId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error managing player ID:', error);
|
console.error('[PlayerManager] Error during legacy migration:', error.message);
|
||||||
return uuidv4();
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getOrCreatePlayerId
|
getOrCreatePlayerId,
|
||||||
|
migrateLegacyPlayerId
|
||||||
};
|
};
|
||||||
|
|||||||
482
docs/UUID_BUGS_FIX_PLAN.md
Normal file
482
docs/UUID_BUGS_FIX_PLAN.md
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
# UUID/Skin Reset Bug Fix Plan
|
||||||
|
|
||||||
|
## Problem Summary
|
||||||
|
|
||||||
|
Players experience random skin/cosmetic resets without intentionally changing anything. The root cause is that the UUID system has multiple failure points that can silently generate new UUIDs or use the wrong UUID during gameplay.
|
||||||
|
|
||||||
|
**Impact**: Players lose their customized cosmetics/skins randomly, causing frustration and confusion.
|
||||||
|
|
||||||
|
**Status**: ✅ **FIXED** - All critical and high priority bugs have been addressed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Summary
|
||||||
|
|
||||||
|
### What Was Fixed
|
||||||
|
|
||||||
|
| Bug | Severity | Status | Description |
|
||||||
|
|-----|----------|--------|-------------|
|
||||||
|
| BUG-001 | Critical | ✅ Fixed | Username not loaded before play click |
|
||||||
|
| BUG-002 | High | ✅ Fixed | isFirstLaunch() always returns true |
|
||||||
|
| BUG-003 | Critical | ✅ Fixed | Silent config corruption returns empty object |
|
||||||
|
| BUG-004 | Critical | ✅ Fixed | Non-atomic config writes |
|
||||||
|
| BUG-005 | High | ✅ Fixed | Username fallback to 'Player' |
|
||||||
|
| BUG-006 | Medium | ✅ Fixed | Launch overwrites username every time |
|
||||||
|
| BUG-007 | Medium | ✅ Fixed | Dual UUID systems (playerManager vs config) |
|
||||||
|
| BUG-008 | High | ✅ Fixed | Error returns random UUID |
|
||||||
|
| BUG-009 | Medium | ✅ Fixed | Username case sensitivity |
|
||||||
|
| BUG-010 | Medium | ⏳ Pending | Migration marks complete on partial failure |
|
||||||
|
| BUG-011 | Medium | ⏳ Pending | Race condition on concurrent config access |
|
||||||
|
| BUG-012 | High | ✅ Fixed | UUID modal isCurrent flag broken |
|
||||||
|
| BUG-013 | High | ✅ Fixed | UUID setting uses unsaved DOM username |
|
||||||
|
| BUG-014 | Medium | ✅ Fixed | No way to switch between saved identities |
|
||||||
|
| BUG-015 | High | ✅ Fixed | installGame saves username (overwrites good value) |
|
||||||
|
| BUG-016 | High | ✅ Fixed | Username rename creates new UUID instead of preserving |
|
||||||
|
| BUG-017 | Medium | ✅ Fixed | UUID list not refreshing when player name changes |
|
||||||
|
| BUG-018 | Low | ✅ Fixed | Custom UUID input doesn't allow copy/paste |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Scenario Analysis
|
||||||
|
|
||||||
|
All user scenarios have been analyzed for UUID/username persistence:
|
||||||
|
|
||||||
|
| Scenario | Risk | Status | Details |
|
||||||
|
|----------|------|--------|---------|
|
||||||
|
| **Fresh Install** | Low | ✅ Safe | firstLaunch.js reads but doesn't modify username/UUID |
|
||||||
|
| **Username Change** | Low | ✅ Safe | Rename preserves UUID, user-initiated saves work correctly |
|
||||||
|
| **Auto-Update** | Low | ✅ Safe | Config is on disk before update, backup recovery available |
|
||||||
|
| **Manual Update** | Low | ✅ Safe | Config file persists across manual updates |
|
||||||
|
| **Different Install Location** | Low | ✅ Safe | Config uses central app directory, not install-relative |
|
||||||
|
| **Repair Game** | Low | ✅ Safe | repairGame() doesn't touch config |
|
||||||
|
| **UUID Modal** | Low | ✅ Fixed | Fixed isCurrent badge, unsaved username bug, added switch button |
|
||||||
|
| **Profile Switch** | Low | ✅ Safe | Profiles only control mods/java, not username/UUID |
|
||||||
|
| **Branch Change** | Low | ✅ Safe | Only changes game version, not identity |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
| File | Changes |
|
||||||
|
|------|---------|
|
||||||
|
| `backend/core/config.js` | Atomic writes, backup/recovery, validation, case-insensitive UUID lookup, checkLaunchReady(), username rename preserves UUID |
|
||||||
|
| `backend/managers/gameLauncher.js` | Pre-launch validation, removed saveUsername call |
|
||||||
|
| `backend/managers/gameManager.js` | Removed saveUsername call from installGame |
|
||||||
|
| `backend/services/playerManager.js` | Marked DEPRECATED, throws on error, retry logic |
|
||||||
|
| `backend/launcher.js` | Export new functions (checkLaunchReady, hasUsername, etc.) |
|
||||||
|
| `GUI/js/launcher.js` | Uses checkLaunchReady API, blocks launch if no username |
|
||||||
|
| `GUI/js/settings.js` | UUID modal fixes, switchToUsername function, proper error handling, refreshes UUID list on name change |
|
||||||
|
| `GUI/style.css` | Switch button styling, user-select: text for UUID input |
|
||||||
|
| `GUI/locales/*.json` | Added translation keys for switch username functionality (all 10 locales) |
|
||||||
|
| `main.js` | Fixed UUID IPC handlers, added checkLaunchReady handler, enabled Ctrl+V/C/X/A shortcuts |
|
||||||
|
| `preload.js` | Exposed checkLaunchReady to renderer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug Categories
|
||||||
|
|
||||||
|
### Category A: Race Conditions & Initialization
|
||||||
|
### Category B: Silent Failures & Fallbacks
|
||||||
|
### Category C: Data Integrity & Persistence
|
||||||
|
### Category D: Design Issues
|
||||||
|
### Category E: UI/UX Issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Bug List & Fixes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-001: Username Not Loaded Before Play Click (CRITICAL) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: A - Race Condition
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
- `GUI/js/launcher.js`
|
||||||
|
- `GUI/js/settings.js`
|
||||||
|
|
||||||
|
**Problem**: If user clicks Play before settings DOM initializes, returns 'Player' silently.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- launcher.js now uses `checkLaunchReady()` API to validate before launch
|
||||||
|
- Loads username from backend config (single source of truth)
|
||||||
|
- Blocks launch and shows error if no username configured
|
||||||
|
- Navigates user to settings page to set username
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-002: `isFirstLaunch()` Always Returns True (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: B - Silent Failure
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js`
|
||||||
|
|
||||||
|
**Problem**: Function always returns `true` even when user has data (typo: `return true` instead of `return false`).
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Fixed return statement: `return true` → `return false`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-003: Silent Config Corruption Returns Empty Object (CRITICAL) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: B - Silent Failure
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js`
|
||||||
|
|
||||||
|
**Problem**: Corrupted config silently returns `{}`, causing UUID regeneration.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Added config validation after load
|
||||||
|
- Implemented backup config system (config.json.bak)
|
||||||
|
- Tries loading backup if primary fails
|
||||||
|
- Logs detailed errors for debugging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-004: Non-Atomic Config Writes (CRITICAL) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: C - Data Integrity
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js`
|
||||||
|
|
||||||
|
**Problem**: Direct write can corrupt file if interrupted. Silent error logging.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Atomic write: write to temp file → verify JSON → backup current → rename
|
||||||
|
- Throws error on save failure (no silent continuation)
|
||||||
|
- Cleans up temp file on failure
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-005: Username Fallback to 'Player' (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: B - Silent Failure
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js`
|
||||||
|
|
||||||
|
**Problem**: Missing username silently falls back to 'Player', causing wrong UUID.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- `loadUsername()` returns `null` instead of 'Player'
|
||||||
|
- Added `loadUsernameWithDefault()` for display purposes
|
||||||
|
- Added `hasUsername()` helper function
|
||||||
|
- All callers updated to handle null case explicitly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-006: Launch Overwrites Username Every Time (MEDIUM) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**: `backend/managers/gameLauncher.js`
|
||||||
|
|
||||||
|
**Problem**: If playerName parameter is wrong, it overwrites the saved username.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Removed `saveUsername()` call from launch process
|
||||||
|
- Username only saved when user explicitly changes it in Settings
|
||||||
|
- Launch loads username from config (single source of truth)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-007: Dual UUID Systems (playerManager vs config) (MEDIUM) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
- `backend/services/playerManager.js` → `player_id.json`
|
||||||
|
- `backend/core/config.js` → `config.json` → `userUuids`
|
||||||
|
|
||||||
|
**Problem**: Two independent UUID systems can desync.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- `playerManager.js` marked as DEPRECATED
|
||||||
|
- All code uses `config.js` `getUuidForUser()`
|
||||||
|
- Migration function added for legacy `player_id.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-008: Error Returns Random UUID (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: B - Silent Failure
|
||||||
|
|
||||||
|
**Location**: `backend/services/playerManager.js`
|
||||||
|
|
||||||
|
**Problem**: Any error generates random UUID, losing player identity.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Now throws error instead of returning random UUID
|
||||||
|
- Retry logic added (3 attempts before failure)
|
||||||
|
- Caller must handle the error appropriately
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-009: Username Case Sensitivity (MEDIUM) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js`
|
||||||
|
|
||||||
|
**Problem**: "PlayerOne" and "playerone" are different UUIDs.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- `getUuidForUser()` uses case-insensitive lookup
|
||||||
|
- Username stored with ORIGINAL case (preserves "Sanasol", "SaAnAsOl", etc.)
|
||||||
|
- Lookup normalized to lowercase for matching
|
||||||
|
- Case changes update the stored key while preserving UUID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-010: Migration Marks Complete Even on Partial Failure (MEDIUM) ⏳ PENDING
|
||||||
|
|
||||||
|
**Category**: C - Data Integrity
|
||||||
|
|
||||||
|
**Location**: `backend/utils/userDataMigration.js`
|
||||||
|
|
||||||
|
**Problem**: Partial copy is marked as complete, preventing retry.
|
||||||
|
|
||||||
|
**Status**: Not yet implemented - low priority since migration runs once.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-011: Race Condition on Concurrent Config Access (MEDIUM) ⏳ PENDING
|
||||||
|
|
||||||
|
**Category**: A - Race Condition
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js`
|
||||||
|
|
||||||
|
**Problem**: No file locking - concurrent processes can overwrite each other.
|
||||||
|
|
||||||
|
**Status**: Not yet implemented - would require `proper-lockfile` package. Low risk since launcher is single-instance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-012: UUID Modal isCurrent Flag Broken (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**: `main.js` - `get-all-uuid-mappings` IPC handler
|
||||||
|
|
||||||
|
**Problem**: Case-sensitive comparison between normalized key (lowercase) and current username.
|
||||||
|
```javascript
|
||||||
|
// BROKEN:
|
||||||
|
isCurrent: username === loadUsername() // "player1" === "Player1" → FALSE
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- IPC handler now uses `getAllUuidMappingsArray()` from config.js
|
||||||
|
- This function correctly compares against normalized username
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-013: UUID Setting Uses Unsaved DOM Username (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: B - Silent Failure
|
||||||
|
|
||||||
|
**Location**: `GUI/js/settings.js` - `performSetCustomUuid()`
|
||||||
|
|
||||||
|
**Problem**: Gets username from DOM input field instead of saved config.
|
||||||
|
```javascript
|
||||||
|
// BROKEN:
|
||||||
|
const username = getCurrentPlayerName(); // From UI input, not saved!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Risk Scenario**: User types new name but doesn't save → opens UUID modal → sets custom UUID → UUID gets set for unsaved name while config has old name.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Now loads username from backend config via `window.electronAPI.loadUsername()`
|
||||||
|
- Shows error if no username is saved
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-014: No Way to Switch Between Saved Identities (MEDIUM) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**: `GUI/js/settings.js` - UUID modal
|
||||||
|
|
||||||
|
**Problem**: UUID modal showed list of usernames/UUIDs but no way to switch to a different identity.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Added `switchToUsername()` function
|
||||||
|
- New switch button (user-check icon) on non-current entries
|
||||||
|
- Confirmation dialog before switching
|
||||||
|
- Updates username input and refreshes UUID display
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-015: installGame Saves Username (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**: `backend/managers/gameManager.js` - `installGame()`
|
||||||
|
|
||||||
|
**Problem**: `saveUsername(playerName)` call could overwrite good username with 'Player' default.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Removed `saveUsername()` call from `installGame()`
|
||||||
|
- Username only saved when user explicitly changes it in Settings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-016: Username Rename Creates New UUID (HIGH) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: D - Design Issue
|
||||||
|
|
||||||
|
**Location**: `backend/core/config.js` - `saveUsername()`
|
||||||
|
|
||||||
|
**Problem**: When user changes their player name, a new UUID was generated instead of preserving the existing one. User's identity (cosmetics/skins) was lost on every name change.
|
||||||
|
|
||||||
|
**Symptom**: Change "Player1" to "NewPlayer" → gets completely new UUID → loses all cosmetics.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- `saveUsername()` now handles UUID mapping renames atomically
|
||||||
|
- When renaming: old username's UUID is moved to new username
|
||||||
|
- When switching to existing identity: uses that identity's existing UUID
|
||||||
|
- Case changes only: updates key casing, preserves UUID
|
||||||
|
- Both username and userUuids saved in single atomic operation
|
||||||
|
|
||||||
|
**Behavior After Fix**:
|
||||||
|
```javascript
|
||||||
|
// Rename: "Player1" → "NewPlayer"
|
||||||
|
// Before: Player1=uuid-123, NewPlayer=uuid-NEW (wrong!)
|
||||||
|
// After: NewPlayer=uuid-123 (same UUID, just renamed)
|
||||||
|
|
||||||
|
// Switch to existing: "Player1" → "ExistingPlayer"
|
||||||
|
// Uses ExistingPlayer's existing UUID (switching identity)
|
||||||
|
|
||||||
|
// Case change: "Player1" → "PLAYER1"
|
||||||
|
// UUID preserved, key updated to new case
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-017: UUID List Not Refreshing on Name Change (MEDIUM) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: E - UI/UX Issue
|
||||||
|
|
||||||
|
**Location**: `GUI/js/settings.js` - `savePlayerName()`
|
||||||
|
|
||||||
|
**Problem**: After changing player name in settings, the UUID modal list didn't refresh. The "Current" badge showed on the old username instead of the new one.
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Added `await loadAllUuids()` call after `loadCurrentUuid()` in `savePlayerName()`
|
||||||
|
- UUID modal now shows correct "Current" badge after name changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### BUG-018: Custom UUID Input Doesn't Allow Copy/Paste (LOW) ✅ FIXED
|
||||||
|
|
||||||
|
**Category**: E - UI/UX Issue
|
||||||
|
|
||||||
|
**Location**: `GUI/style.css`, `main.js`
|
||||||
|
|
||||||
|
**Problem**: Two issues prevented copy/paste:
|
||||||
|
1. The body element has `select-none` class (Tailwind) which applies `user-select: none` globally
|
||||||
|
2. Electron's `setIgnoreMenuShortcuts(true)` was blocking Ctrl+V/C/X/A shortcuts
|
||||||
|
|
||||||
|
**Fix Applied**:
|
||||||
|
- Added `user-select: text` with all vendor prefixes to `.uuid-input` class
|
||||||
|
- Removed `setIgnoreMenuShortcuts(true)` from main.js
|
||||||
|
- Added early return in `before-input-event` handler to allow Ctrl/Cmd + V/C/X/A shortcuts
|
||||||
|
- DevTools shortcuts (Ctrl+Shift+I/J/C, F12) remain blocked
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Translation Keys Added
|
||||||
|
|
||||||
|
The following translation keys were added to `GUI/locales/en.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"notifications": {
|
||||||
|
"noUsername": "No username configured. Please save your username first.",
|
||||||
|
"switchUsernameSuccess": "Switched to \"{username}\" successfully!",
|
||||||
|
"switchUsernameFailed": "Failed to switch username",
|
||||||
|
"playerNameTooLong": "Player name must be 16 characters or less"
|
||||||
|
},
|
||||||
|
"confirm": {
|
||||||
|
"switchUsernameTitle": "Switch Identity",
|
||||||
|
"switchUsernameMessage": "Switch to username \"{username}\"? This will change your current player identity.",
|
||||||
|
"switchUsernameButton": "Switch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
After implementing fixes, verify:
|
||||||
|
|
||||||
|
- [x] Launch with freshly installed launcher - UUID persists
|
||||||
|
- [x] Change username in settings - UUID preserved (renamed, not new)
|
||||||
|
- [x] Config corruption - recovers from backup
|
||||||
|
- [x] Click Play immediately after opening - correct UUID used
|
||||||
|
- [x] Manual update from GitHub - UUID persists
|
||||||
|
- [x] Username with different casing - same UUID used, case preserved
|
||||||
|
- [x] UUID modal shows correct "Current" badge
|
||||||
|
- [x] UUID modal refreshes after username change
|
||||||
|
- [x] Switch identity from UUID modal works
|
||||||
|
- [x] Profile switching doesn't affect username/UUID
|
||||||
|
- [x] Custom UUID input allows copy/paste
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture: How UUID/Username Persistence Works
|
||||||
|
|
||||||
|
**Config Structure** (`config.json`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "CurrentPlayer",
|
||||||
|
"userUuids": {
|
||||||
|
"Sanasol": "uuid-123-abc",
|
||||||
|
"SaAnAsOl": "uuid-456-def",
|
||||||
|
"Player1": "uuid-789-ghi"
|
||||||
|
},
|
||||||
|
"hasLaunchedBefore": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Design Decisions**:
|
||||||
|
- Username stored with ORIGINAL case (e.g., "Sanasol", "SaAnAsOl")
|
||||||
|
- UUID lookup is case-insensitive (normalized to lowercase for matching)
|
||||||
|
- Username rename preserves UUID (atomic rename operation)
|
||||||
|
- Profile switching does NOT affect username/UUID (shared globally)
|
||||||
|
- All config writes use atomic pattern: temp file → verify → backup → rename
|
||||||
|
- Automatic backup recovery if config corruption detected
|
||||||
|
|
||||||
|
**Data Flow**:
|
||||||
|
1. User sets username in Settings → `saveUsername()` handles rename logic → saves to config.json
|
||||||
|
2. If renaming: UUID moved from old name to new name (same UUID preserved)
|
||||||
|
3. Launch game → `checkLaunchReady()` validates username exists
|
||||||
|
4. Launch game → `getUuidForUser(username)` gets UUID (case-insensitive lookup)
|
||||||
|
5. UUID modal → shows all username→UUID mappings from config
|
||||||
|
6. Switch identity → saves new username → gets that username's UUID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- ✅ Zero silent UUID regeneration
|
||||||
|
- ✅ Config corruption recovery working
|
||||||
|
- ✅ No UUID change without explicit user action
|
||||||
|
- ✅ Username rename preserves UUID
|
||||||
|
- ✅ Username case is preserved in display
|
||||||
|
- ✅ UUID modal correctly identifies current user
|
||||||
|
- ✅ UUID modal refreshes on changes
|
||||||
|
- ✅ Users can switch between saved identities
|
||||||
|
- ✅ Copy/paste works in UUID input
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remaining Work
|
||||||
|
|
||||||
|
1. **BUG-010**: Verify migration completeness before marking done (low priority)
|
||||||
|
2. **BUG-011**: Add file locking with `proper-lockfile` (low priority - single instance)
|
||||||
|
3. Add telemetry for config load failures and UUID regeneration events
|
||||||
|
|
||||||
|
## Completed Additional Tasks
|
||||||
|
|
||||||
|
- ✅ Added translation keys to all 10 locale files (de-DE, es-ES, fr-FR, id-ID, pl-PL, pt-BR, ru-RU, sv-SE, tr-TR, en)
|
||||||
41
main.js
41
main.js
@@ -3,7 +3,7 @@ require('dotenv').config({ path: path.join(__dirname, '.env') });
|
|||||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
||||||
const { autoUpdater } = require('electron-updater');
|
const { autoUpdater } = require('electron-updater');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched, loadConfig, saveConfig } = require('./backend/launcher');
|
const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched, loadConfig, saveConfig, checkLaunchReady } = require('./backend/launcher');
|
||||||
const { retryPWRDownload } = require('./backend/managers/gameManager');
|
const { retryPWRDownload } = require('./backend/managers/gameManager');
|
||||||
const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration');
|
const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration');
|
||||||
|
|
||||||
@@ -257,6 +257,17 @@ function createWindow() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||||
|
// Allow standard copy/paste/cut/select-all shortcuts
|
||||||
|
const isMac = process.platform === 'darwin';
|
||||||
|
const modKey = isMac ? input.meta : input.control;
|
||||||
|
const key = input.key.toLowerCase();
|
||||||
|
|
||||||
|
// Allow Ctrl/Cmd + V (paste), C (copy), X (cut), A (select all)
|
||||||
|
if (modKey && !input.shift && ['v', 'c', 'x', 'a'].includes(key)) {
|
||||||
|
return; // Don't block these
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block devtools shortcuts
|
||||||
if (input.control && input.shift && input.key.toLowerCase() === 'i') {
|
if (input.control && input.shift && input.key.toLowerCase() === 'i') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -274,7 +285,6 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Close application shortcuts
|
// Close application shortcuts
|
||||||
const isMac = process.platform === 'darwin';
|
|
||||||
const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') ||
|
const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') ||
|
||||||
(!isMac && input.control && input.key.toLowerCase() === 'q') ||
|
(!isMac && input.control && input.key.toLowerCase() === 'q') ||
|
||||||
(!isMac && input.alt && input.key === 'F4');
|
(!isMac && input.alt && input.key === 'F4');
|
||||||
@@ -290,7 +300,7 @@ function createWindow() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.setIgnoreMenuShortcuts(true);
|
// Note: Not using setIgnoreMenuShortcuts to allow copy/paste to work
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
@@ -596,14 +606,26 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath,
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('save-username', (event, username) => {
|
ipcMain.handle('save-username', (event, username) => {
|
||||||
saveUsername(username);
|
try {
|
||||||
return { success: true };
|
saveUsername(username);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Main] Failed to save username:', error.message);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('load-username', () => {
|
ipcMain.handle('load-username', () => {
|
||||||
|
// Returns null if no username configured (no silent 'Player' fallback)
|
||||||
return loadUsername();
|
return loadUsername();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('check-launch-ready', () => {
|
||||||
|
// Returns launch readiness state with detailed info
|
||||||
|
// { ready: boolean, hasUsername: boolean, username: string|null, issues: string[] }
|
||||||
|
return checkLaunchReady();
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('save-java-path', (event, javaPath) => {
|
ipcMain.handle('save-java-path', (event, javaPath) => {
|
||||||
saveJavaPath(javaPath);
|
saveJavaPath(javaPath);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -1235,12 +1257,9 @@ ipcMain.handle('get-current-uuid', async () => {
|
|||||||
|
|
||||||
ipcMain.handle('get-all-uuid-mappings', async () => {
|
ipcMain.handle('get-all-uuid-mappings', async () => {
|
||||||
try {
|
try {
|
||||||
const mappings = getAllUuidMappings();
|
// Use getAllUuidMappingsArray which correctly normalizes username for comparison
|
||||||
return Object.entries(mappings).map(([username, uuid]) => ({
|
const { getAllUuidMappingsArray } = require('./backend/launcher');
|
||||||
username,
|
return getAllUuidMappingsArray();
|
||||||
uuid,
|
|
||||||
isCurrent: username === require('./backend/launcher').loadUsername()
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting UUID mappings:', error);
|
console.error('Error getting UUID mappings:', error);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getVersion: () => ipcRenderer.invoke('get-version'),
|
getVersion: () => ipcRenderer.invoke('get-version'),
|
||||||
saveUsername: (username) => ipcRenderer.invoke('save-username', username),
|
saveUsername: (username) => ipcRenderer.invoke('save-username', username),
|
||||||
loadUsername: () => ipcRenderer.invoke('load-username'),
|
loadUsername: () => ipcRenderer.invoke('load-username'),
|
||||||
|
checkLaunchReady: () => ipcRenderer.invoke('check-launch-ready'),
|
||||||
saveJavaPath: (javaPath) => ipcRenderer.invoke('save-java-path', javaPath),
|
saveJavaPath: (javaPath) => ipcRenderer.invoke('save-java-path', javaPath),
|
||||||
loadJavaPath: () => ipcRenderer.invoke('load-java-path'),
|
loadJavaPath: () => ipcRenderer.invoke('load-java-path'),
|
||||||
saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath),
|
saveInstallPath: (installPath) => ipcRenderer.invoke('save-install-path', installPath),
|
||||||
|
|||||||
Reference in New Issue
Block a user