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>
This commit is contained in:
sanasol
2026-01-31 19:42:01 +01:00
parent 7b2acd49b6
commit 74f99d0aaf
12 changed files with 1476 additions and 163 deletions

View File

@@ -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
};

View 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,

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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
};