mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-26 06:41:47 -03:00
fix: comprehensive UUID/username persistence bug fixes (#252)
* fix: comprehensive UUID/username persistence bug fixes Major fixes for UUID/skin reset issues that caused players to lose cosmetics: Core fixes: - Username rename now preserves UUID (atomic rename, not new identity) - Atomic config writes with backup/recovery system - Case-insensitive UUID lookup with case-preserving storage - Pre-launch validation blocks play if no username configured - Removed saveUsername calls from launch/install flows UUID Modal fixes: - Fixed isCurrent badge showing on wrong user - Added switch identity button to change between saved usernames - Fixed custom UUID input using unsaved DOM username - UUID list now refreshes when player name changes - Enabled copy/paste in custom UUID input field UI/UX improvements: - Added translation keys for switch username functionality - CSS user-select fix for UUID input fields - Allowed Ctrl+V/C/X/A shortcuts in Electron Files: config.js, gameLauncher.js, gameManager.js, playerManager.js, launcher.js, settings.js, main.js, preload.js, style.css, en.json See UUID_BUGS_FIX_PLAN.md for detailed bug list (18 bugs, 16 fixed) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(i18n): add switch username translations to all locales Added translation keys for username switching functionality: - notifications.noUsername - notifications.switchUsernameSuccess - notifications.switchUsernameFailed - notifications.playerNameTooLong - confirm.switchUsernameTitle - confirm.switchUsernameMessage - confirm.switchUsernameButton Languages updated: de-DE, es-ES, fr-FR, id-ID, pl-PL, pt-BR, ru-RU, sv-SE, tr-TR Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: move UUID_BUGS_FIX_PLAN.md to docs folder * docs: update UUID_BUGS_FIX_PLAN with complete fix details --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,9 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// =============================================================================
|
||||
// UUID PERSISTENCE FIX - Atomic writes, backups, validation
|
||||
// =============================================================================
|
||||
|
||||
// Default auth domain - can be overridden by env var or config
|
||||
const DEFAULT_AUTH_DOMAIN = 'auth.sanasol.ws';
|
||||
@@ -49,57 +52,443 @@ function getAppDir() {
|
||||
}
|
||||
|
||||
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() {
|
||||
// Try primary config first
|
||||
try {
|
||||
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) {
|
||||
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 {};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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) {
|
||||
try {
|
||||
const configDir = path.dirname(CONFIG_FILE);
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
const maxRetries = 3;
|
||||
let lastError;
|
||||
|
||||
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) {
|
||||
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() {
|
||||
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) {
|
||||
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 userUuids = config.userUuids || {};
|
||||
|
||||
if (userUuids[username]) {
|
||||
return userUuids[username];
|
||||
// Case-insensitive lookup - find existing key regardless of case
|
||||
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();
|
||||
userUuids[username] = newUuid;
|
||||
userUuids[displayName] = newUuid;
|
||||
saveConfig({ userUuids });
|
||||
console.log(`[Config] Created new UUID for "${displayName}": ${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) {
|
||||
const trimmed = (javaPath || '').trim();
|
||||
saveConfig({ javaPath: trimmed });
|
||||
@@ -120,6 +509,10 @@ function loadJavaPath() {
|
||||
return config.javaPath || '';
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INSTALL PATH MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
function saveInstallPath(installPath) {
|
||||
const trimmed = (installPath || '').trim();
|
||||
saveConfig({ installPath: trimmed });
|
||||
@@ -130,6 +523,10 @@ function loadInstallPath() {
|
||||
return config.installPath || '';
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DISCORD RPC SETTINGS
|
||||
// =============================================================================
|
||||
|
||||
function saveDiscordRPC(enabled) {
|
||||
saveConfig({ discordRPC: !!enabled });
|
||||
}
|
||||
@@ -139,6 +536,10 @@ function loadDiscordRPC() {
|
||||
return config.discordRPC !== undefined ? config.discordRPC : true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LANGUAGE SETTINGS
|
||||
// =============================================================================
|
||||
|
||||
function saveLanguage(language) {
|
||||
saveConfig({ language: language || 'en' });
|
||||
}
|
||||
@@ -148,6 +549,10 @@ function loadLanguage() {
|
||||
return config.language || 'en';
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LAUNCHER SETTINGS
|
||||
// =============================================================================
|
||||
|
||||
function saveCloseLauncherOnStart(enabled) {
|
||||
saveConfig({ closeLauncherOnStart: !!enabled });
|
||||
}
|
||||
@@ -166,31 +571,38 @@ function loadLauncherHardwareAcceleration() {
|
||||
return config.launcherHardwareAcceleration !== undefined ? config.launcherHardwareAcceleration : true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MODS MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
function saveModsToConfig(mods) {
|
||||
try {
|
||||
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]) {
|
||||
config.profiles[config.activeProfileId].mods = mods;
|
||||
} else {
|
||||
// Fallback for legacy or no-profile state
|
||||
config.installedMods = mods;
|
||||
}
|
||||
|
||||
// Use atomic save
|
||||
const configDir = path.dirname(CONFIG_FILE);
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||||
console.log('Mods saved to config.json');
|
||||
// Write atomically
|
||||
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) {
|
||||
console.error('Error saving mods to config:', error);
|
||||
console.error('[Config] Error saving mods:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,25 +610,34 @@ function loadModsFromConfig() {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
|
||||
// Prefer Active Profile
|
||||
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
||||
return config.profiles[config.activeProfileId].mods || [];
|
||||
}
|
||||
|
||||
return config.installedMods || [];
|
||||
} catch (error) {
|
||||
console.error('Error loading mods from config:', error);
|
||||
console.error('[Config] Error loading mods:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// FIRST LAUNCH DETECTION - FIXED
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Check if this is the first launch
|
||||
* FIXED: Was always returning true due to bug
|
||||
*/
|
||||
function isFirstLaunch() {
|
||||
const config = loadConfig();
|
||||
|
||||
// If explicitly marked, use that
|
||||
if ('hasLaunchedBefore' in config) {
|
||||
return !config.hasLaunchedBefore;
|
||||
}
|
||||
|
||||
// Check for any existing user data
|
||||
const hasUserData = config.installPath || config.username || config.javaPath ||
|
||||
config.chatUsername || config.userUuids ||
|
||||
Object.keys(config).length > 0;
|
||||
@@ -225,65 +646,17 @@ function isFirstLaunch() {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
// FIXED: Was returning true here, should be false
|
||||
return false;
|
||||
}
|
||||
|
||||
function markAsLaunched() {
|
||||
saveConfig({ hasLaunchedBefore: true, firstLaunchDate: new Date().toISOString() });
|
||||
}
|
||||
|
||||
// UUID Management Functions
|
||||
function getCurrentUuid() {
|
||||
const username = loadUsername();
|
||||
return getUuidForUser(username);
|
||||
}
|
||||
|
||||
function getAllUuidMappings() {
|
||||
const config = loadConfig();
|
||||
return config.userUuids || {};
|
||||
}
|
||||
|
||||
function setUuidForUser(username, uuid) {
|
||||
const { v4: uuidv4, validate: validateUuid } = require('uuid');
|
||||
|
||||
// Validate UUID format
|
||||
if (!validateUuid(uuid)) {
|
||||
throw new Error('Invalid UUID format');
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
const userUuids = config.userUuids || {};
|
||||
userUuids[username] = uuid;
|
||||
saveConfig({ userUuids });
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
function generateNewUuid() {
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
return uuidv4();
|
||||
}
|
||||
|
||||
function deleteUuidForUser(username) {
|
||||
const config = loadConfig();
|
||||
const userUuids = config.userUuids || {};
|
||||
|
||||
if (userUuids[username]) {
|
||||
delete userUuids[username];
|
||||
saveConfig({ userUuids });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function resetCurrentUserUuid() {
|
||||
const username = loadUsername();
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const newUuid = uuidv4();
|
||||
|
||||
return setUuidForUser(username, newUuid);
|
||||
}
|
||||
// =============================================================================
|
||||
// GPU PREFERENCE
|
||||
// =============================================================================
|
||||
|
||||
function saveGpuPreference(gpuPreference) {
|
||||
saveConfig({ gpuPreference: gpuPreference || 'auto' });
|
||||
@@ -294,6 +667,10 @@ function loadGpuPreference() {
|
||||
return config.gpuPreference || 'auto';
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// VERSION MANAGEMENT
|
||||
// =============================================================================
|
||||
|
||||
function saveVersionClient(versionClient) {
|
||||
saveConfig({ version_client: versionClient });
|
||||
}
|
||||
@@ -306,7 +683,7 @@ function loadVersionClient() {
|
||||
function saveVersionBranch(versionBranch) {
|
||||
const branch = versionBranch || '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' });
|
||||
} else {
|
||||
saveConfig({ version_branch: branch });
|
||||
@@ -318,50 +695,98 @@ function loadVersionBranch() {
|
||||
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 = {
|
||||
// Core config
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
validateConfig,
|
||||
|
||||
// Username (no silent fallbacks)
|
||||
saveUsername,
|
||||
loadUsername,
|
||||
loadUsernameWithDefault,
|
||||
hasUsername,
|
||||
|
||||
// UUID management
|
||||
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,
|
||||
getAllUuidMappings,
|
||||
getAllUuidMappingsArray,
|
||||
setUuidForUser,
|
||||
generateNewUuid,
|
||||
deleteUuidForUser,
|
||||
resetCurrentUserUuid,
|
||||
// GPU Preference exports
|
||||
saveGpuPreference,
|
||||
loadGpuPreference,
|
||||
// Close Launcher export
|
||||
|
||||
// Java/Install paths
|
||||
saveJavaPath,
|
||||
loadJavaPath,
|
||||
saveInstallPath,
|
||||
loadInstallPath,
|
||||
|
||||
// Settings
|
||||
saveDiscordRPC,
|
||||
loadDiscordRPC,
|
||||
saveLanguage,
|
||||
loadLanguage,
|
||||
saveCloseLauncherOnStart,
|
||||
loadCloseLauncherOnStart,
|
||||
|
||||
// Hardware Acceleration functions
|
||||
saveLauncherHardwareAcceleration,
|
||||
loadLauncherHardwareAcceleration,
|
||||
|
||||
// Version Management exports
|
||||
// Mods
|
||||
saveModsToConfig,
|
||||
loadModsFromConfig,
|
||||
|
||||
// Launch state
|
||||
isFirstLaunch,
|
||||
markAsLaunched,
|
||||
checkLaunchReady,
|
||||
|
||||
// Auth server
|
||||
getAuthServerUrl,
|
||||
getAuthDomain,
|
||||
saveAuthDomain,
|
||||
|
||||
// GPU
|
||||
saveGpuPreference,
|
||||
loadGpuPreference,
|
||||
|
||||
// Version
|
||||
saveVersionClient,
|
||||
loadVersionClient,
|
||||
saveVersionBranch,
|
||||
loadVersionBranch
|
||||
loadVersionBranch,
|
||||
|
||||
// Constants
|
||||
CONFIG_FILE
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
const {
|
||||
saveUsername,
|
||||
loadUsername,
|
||||
loadUsernameWithDefault,
|
||||
hasUsername,
|
||||
saveJavaPath,
|
||||
loadJavaPath,
|
||||
saveInstallPath,
|
||||
@@ -27,9 +29,11 @@ const {
|
||||
getUuidForUser,
|
||||
isFirstLaunch,
|
||||
markAsLaunched,
|
||||
checkLaunchReady,
|
||||
// UUID Management
|
||||
getCurrentUuid,
|
||||
getAllUuidMappings,
|
||||
getAllUuidMappingsArray,
|
||||
setUuidForUser,
|
||||
generateNewUuid,
|
||||
deleteUuidForUser,
|
||||
@@ -110,7 +114,10 @@ module.exports = {
|
||||
// User configuration functions
|
||||
saveUsername,
|
||||
loadUsername,
|
||||
loadUsernameWithDefault,
|
||||
hasUsername,
|
||||
getUuidForUser,
|
||||
checkLaunchReady,
|
||||
|
||||
// Java configuration functions
|
||||
saveJavaPath,
|
||||
@@ -162,6 +169,7 @@ module.exports = {
|
||||
// UUID Management functions
|
||||
getCurrentUuid,
|
||||
getAllUuidMappings,
|
||||
getAllUuidMappingsArray,
|
||||
setUuidForUser,
|
||||
generateNewUuid,
|
||||
deleteUuidForUser,
|
||||
|
||||
@@ -7,7 +7,19 @@ const { spawn } = require('child_process');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { getResolvedAppDir, findClientPath } = require('../core/paths');
|
||||
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 { getLatestClientVersion } = require('../services/versionManager');
|
||||
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) {
|
||||
// Synchronize server list on every game launch
|
||||
async function launchGame(playerNameOverride = null, progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) {
|
||||
// ==========================================================================
|
||||
// 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 {
|
||||
console.log('[Launcher] Synchronizing server list...');
|
||||
await syncServerList();
|
||||
@@ -113,11 +159,14 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
||||
console.warn('[Launcher] Server list sync failed, continuing launch:', syncError.message);
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// STEP 3: Setup paths and directories
|
||||
// ==========================================================================
|
||||
const branch = branchOverride || loadVersionBranch();
|
||||
const customAppDir = getResolvedAppDir(installPathOverride);
|
||||
const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
|
||||
const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
|
||||
|
||||
|
||||
// NEW 2.2.0: Use centralized UserData location
|
||||
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.');
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
// ==========================================================================
|
||||
// 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();
|
||||
|
||||
|
||||
if (progressCallback) {
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
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) {
|
||||
saveInstallPath(installPathOverride);
|
||||
}
|
||||
|
||||
@@ -3,32 +3,117 @@ const path = require('path');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { PLAYER_ID_FILE, APP_DIR } = require('../core/paths');
|
||||
|
||||
function getOrCreatePlayerId() {
|
||||
try {
|
||||
if (!fs.existsSync(APP_DIR)) {
|
||||
fs.mkdirSync(APP_DIR, { recursive: true });
|
||||
}
|
||||
/**
|
||||
* DEPRECATED: This file is kept for backward compatibility.
|
||||
*
|
||||
* 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'));
|
||||
if (data.playerId) {
|
||||
return data.playerId;
|
||||
/**
|
||||
* Get or create a legacy player ID
|
||||
* NOTE: This is DEPRECATED - use config.js getUuidForUser() instead
|
||||
*
|
||||
* 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();
|
||||
fs.writeFileSync(PLAYER_ID_FILE, JSON.stringify({
|
||||
playerId: newPlayerId,
|
||||
createdAt: new Date().toISOString()
|
||||
}, null, 2));
|
||||
// FIXED: Do NOT return random UUID - throw error instead
|
||||
// Returning random UUID was causing silent identity loss
|
||||
console.error('[PlayerManager] CRITICAL: Failed to get/create player ID after all retries');
|
||||
throw new Error(`Failed to manage player ID: ${lastError.message}`);
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error('Error managing player ID:', error);
|
||||
return uuidv4();
|
||||
console.error('[PlayerManager] Error during legacy migration:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOrCreatePlayerId
|
||||
getOrCreatePlayerId,
|
||||
migrateLegacyPlayerId
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user