mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 12:51:47 -03:00
Players were losing character data (inventory, armor, backpack) after each launcher update because config.json corruption wiped the UUID mapping. Same username, new UUID = server treats as new player. Fix: UUIDs now stored in separate uuid-store.json that saveConfig() can never touch. Added safety check to refuse destructive writes when config file exists but loads empty. Includes 28 regression tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
524 lines
20 KiB
JavaScript
524 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* UUID Persistence Tests
|
|
*
|
|
* Simulates the exact conditions that caused character data loss:
|
|
* - Config file corruption during updates
|
|
* - File locks making config temporarily unreadable
|
|
* - Username re-entry after config wipe
|
|
*
|
|
* Run: node test-uuid-persistence.js
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
|
|
// Use a temp directory so we don't mess with real config
|
|
const TEST_DIR = path.join(os.tmpdir(), 'hytale-uuid-test-' + Date.now());
|
|
const CONFIG_FILE = path.join(TEST_DIR, 'config.json');
|
|
const CONFIG_BACKUP = path.join(TEST_DIR, 'config.json.bak');
|
|
const CONFIG_TEMP = path.join(TEST_DIR, 'config.json.tmp');
|
|
const UUID_STORE_FILE = path.join(TEST_DIR, 'uuid-store.json');
|
|
|
|
// Track test results
|
|
let passed = 0;
|
|
let failed = 0;
|
|
const failures = [];
|
|
|
|
function assert(condition, message) {
|
|
if (condition) {
|
|
passed++;
|
|
console.log(` ✓ ${message}`);
|
|
} else {
|
|
failed++;
|
|
failures.push(message);
|
|
console.log(` ✗ FAIL: ${message}`);
|
|
}
|
|
}
|
|
|
|
function assertEqual(actual, expected, message) {
|
|
if (actual === expected) {
|
|
passed++;
|
|
console.log(` ✓ ${message}`);
|
|
} else {
|
|
failed++;
|
|
failures.push(`${message} (expected: ${expected}, got: ${actual})`);
|
|
console.log(` ✗ FAIL: ${message} (expected: "${expected}", got: "${actual}")`);
|
|
}
|
|
}
|
|
|
|
function cleanup() {
|
|
try {
|
|
if (fs.existsSync(TEST_DIR)) {
|
|
fs.rmSync(TEST_DIR, { recursive: true });
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
function setup() {
|
|
cleanup();
|
|
fs.mkdirSync(TEST_DIR, { recursive: true });
|
|
}
|
|
|
|
// ============================================================================
|
|
// Inline the config functions so we can override paths
|
|
// (We can't require config.js directly because it uses hardcoded getAppDir())
|
|
// ============================================================================
|
|
|
|
function validateConfig(config) {
|
|
if (!config || typeof config !== 'object') return false;
|
|
if (config.userUuids !== undefined && typeof config.userUuids !== 'object') return false;
|
|
if (config.username !== undefined && (typeof config.username !== 'string')) return false;
|
|
return true;
|
|
}
|
|
|
|
function loadConfig() {
|
|
try {
|
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
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.error('[Config] Failed to load primary config:', err.message);
|
|
}
|
|
|
|
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');
|
|
try { fs.writeFileSync(CONFIG_FILE, data, 'utf8'); } catch (e) {}
|
|
return config;
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {}
|
|
|
|
return {};
|
|
}
|
|
|
|
function saveConfig(update) {
|
|
const maxRetries = 3;
|
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
if (!fs.existsSync(TEST_DIR)) fs.mkdirSync(TEST_DIR, { recursive: true });
|
|
|
|
const currentConfig = loadConfig();
|
|
|
|
// SAFETY CHECK: refuse to save if file exists but loaded empty
|
|
if (Object.keys(currentConfig).length === 0 && fs.existsSync(CONFIG_FILE)) {
|
|
const fileSize = fs.statSync(CONFIG_FILE).size;
|
|
if (fileSize > 2) {
|
|
console.error(`[Config] REFUSING to save — loaded empty but file exists (${fileSize} bytes). Retrying...`);
|
|
const delay = attempt * 50; // shorter delay for tests
|
|
const start = Date.now();
|
|
while (Date.now() - start < delay) {}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const newConfig = { ...currentConfig, ...update };
|
|
const data = JSON.stringify(newConfig, null, 2);
|
|
|
|
fs.writeFileSync(CONFIG_TEMP, data, 'utf8');
|
|
const verification = JSON.parse(fs.readFileSync(CONFIG_TEMP, 'utf8'));
|
|
if (!validateConfig(verification)) throw new Error('Validation failed');
|
|
|
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
try {
|
|
const currentData = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
if (currentData.trim()) fs.writeFileSync(CONFIG_BACKUP, currentData, 'utf8');
|
|
} catch (e) {}
|
|
}
|
|
|
|
fs.renameSync(CONFIG_TEMP, CONFIG_FILE);
|
|
return true;
|
|
} catch (err) {
|
|
try { if (fs.existsSync(CONFIG_TEMP)) fs.unlinkSync(CONFIG_TEMP); } catch (e) {}
|
|
if (attempt >= maxRetries) throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
function loadUuidStore() {
|
|
try {
|
|
if (fs.existsSync(UUID_STORE_FILE)) {
|
|
const data = fs.readFileSync(UUID_STORE_FILE, 'utf8');
|
|
if (data.trim()) return JSON.parse(data);
|
|
}
|
|
} catch (err) {}
|
|
return {};
|
|
}
|
|
|
|
function saveUuidStore(store) {
|
|
const tmpFile = UUID_STORE_FILE + '.tmp';
|
|
fs.writeFileSync(tmpFile, JSON.stringify(store, null, 2), 'utf8');
|
|
fs.renameSync(tmpFile, UUID_STORE_FILE);
|
|
}
|
|
|
|
function migrateUuidStoreIfNeeded() {
|
|
if (fs.existsSync(UUID_STORE_FILE)) return;
|
|
const config = loadConfig();
|
|
if (config.userUuids && Object.keys(config.userUuids).length > 0) {
|
|
console.log('[UUID Store] Migrating', Object.keys(config.userUuids).length, 'UUIDs');
|
|
saveUuidStore(config.userUuids);
|
|
}
|
|
}
|
|
|
|
function getUuidForUser(username) {
|
|
const { v4: uuidv4 } = require('uuid');
|
|
if (!username || !username.trim()) throw new Error('Username required');
|
|
|
|
const displayName = username.trim();
|
|
const normalizedLookup = displayName.toLowerCase();
|
|
|
|
migrateUuidStoreIfNeeded();
|
|
|
|
// 1. Check UUID store (source of truth)
|
|
const uuidStore = loadUuidStore();
|
|
const storeKey = Object.keys(uuidStore).find(k => k.toLowerCase() === normalizedLookup);
|
|
if (storeKey) {
|
|
const existingUuid = uuidStore[storeKey];
|
|
if (storeKey !== displayName) {
|
|
delete uuidStore[storeKey];
|
|
uuidStore[displayName] = existingUuid;
|
|
saveUuidStore(uuidStore);
|
|
}
|
|
// Sync to config (non-critical)
|
|
try {
|
|
const config = loadConfig();
|
|
const configUuids = config.userUuids || {};
|
|
const configKey = Object.keys(configUuids).find(k => k.toLowerCase() === normalizedLookup);
|
|
if (!configKey || configUuids[configKey] !== existingUuid) {
|
|
if (configKey) delete configUuids[configKey];
|
|
configUuids[displayName] = existingUuid;
|
|
saveConfig({ userUuids: configUuids });
|
|
}
|
|
} catch (e) {}
|
|
return existingUuid;
|
|
}
|
|
|
|
// 2. Fallback: check config.json
|
|
const config = loadConfig();
|
|
const userUuids = config.userUuids || {};
|
|
const configKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
|
if (configKey) {
|
|
const recoveredUuid = userUuids[configKey];
|
|
uuidStore[displayName] = recoveredUuid;
|
|
saveUuidStore(uuidStore);
|
|
return recoveredUuid;
|
|
}
|
|
|
|
// 3. New user — generate UUID
|
|
const newUuid = uuidv4();
|
|
uuidStore[displayName] = newUuid;
|
|
saveUuidStore(uuidStore);
|
|
userUuids[displayName] = newUuid;
|
|
saveConfig({ userUuids });
|
|
return newUuid;
|
|
}
|
|
|
|
// ============================================================================
|
|
// OLD CODE (before fix) — for comparison testing
|
|
// ============================================================================
|
|
|
|
function getUuidForUser_OLD(username) {
|
|
const { v4: uuidv4 } = require('uuid');
|
|
if (!username || !username.trim()) throw new Error('Username required');
|
|
const displayName = username.trim();
|
|
const normalizedLookup = displayName.toLowerCase();
|
|
|
|
const config = loadConfig();
|
|
const userUuids = config.userUuids || {};
|
|
const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
|
|
|
if (existingKey) {
|
|
return userUuids[existingKey];
|
|
}
|
|
|
|
// New user
|
|
const newUuid = uuidv4();
|
|
userUuids[displayName] = newUuid;
|
|
saveConfig({ userUuids });
|
|
return newUuid;
|
|
}
|
|
|
|
function saveConfig_OLD(update) {
|
|
// OLD saveConfig without safety check
|
|
if (!fs.existsSync(TEST_DIR)) fs.mkdirSync(TEST_DIR, { recursive: true });
|
|
const currentConfig = loadConfig();
|
|
// NO SAFETY CHECK — this is the bug
|
|
const newConfig = { ...currentConfig, ...update };
|
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2), 'utf8');
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// TESTS
|
|
// ============================================================================
|
|
|
|
console.log('\n' + '='.repeat(70));
|
|
console.log('UUID PERSISTENCE TESTS — Simulating update corruption scenarios');
|
|
console.log('='.repeat(70));
|
|
|
|
// --------------------------------------------------------------------------
|
|
// TEST 1: Normal flow — UUID stays consistent
|
|
// --------------------------------------------------------------------------
|
|
console.log('\n--- Test 1: Normal flow — UUID stays consistent ---');
|
|
setup();
|
|
|
|
const uuid1 = getUuidForUser('SpecialK');
|
|
const uuid2 = getUuidForUser('SpecialK');
|
|
const uuid3 = getUuidForUser('specialk'); // case insensitive
|
|
|
|
assertEqual(uuid1, uuid2, 'Same username returns same UUID');
|
|
assertEqual(uuid1, uuid3, 'Case-insensitive lookup returns same UUID');
|
|
assert(uuid1.match(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i), 'UUID is valid v4 format');
|
|
|
|
// --------------------------------------------------------------------------
|
|
// TEST 2: Simulate update corruption (THE BUG) — old code
|
|
// --------------------------------------------------------------------------
|
|
console.log('\n--- Test 2: OLD CODE — Config wipe during update loses UUID ---');
|
|
setup();
|
|
|
|
// Setup: player has UUID
|
|
const oldConfig = { username: 'SpecialK', userUuids: { 'SpecialK': 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' }, hasLaunchedBefore: true };
|
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(oldConfig, null, 2), 'utf8');
|
|
|
|
const uuidBefore = getUuidForUser_OLD('SpecialK');
|
|
assertEqual(uuidBefore, 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'UUID correct before corruption');
|
|
|
|
// Simulate: config.json gets corrupted (loadConfig returns {} because file locked)
|
|
// This simulates what happens when saveConfig reads an empty/locked file
|
|
fs.writeFileSync(CONFIG_FILE, '', 'utf8'); // Simulate corruption: empty file
|
|
|
|
// Old saveConfig behavior: reads empty, merges with update, saves
|
|
// This wipes userUuids
|
|
saveConfig_OLD({ hasLaunchedBefore: true });
|
|
|
|
const configAfterCorruption = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
assert(!configAfterCorruption.userUuids, 'OLD CODE: userUuids wiped after corruption');
|
|
assert(!configAfterCorruption.username, 'OLD CODE: username wiped after corruption');
|
|
|
|
// Player re-enters name, gets NEW UUID (character data lost!)
|
|
const uuidAfterOld = getUuidForUser_OLD('SpecialK');
|
|
assert(uuidAfterOld !== uuidBefore, 'OLD CODE: UUID changed after corruption (BUG!)');
|
|
|
|
// --------------------------------------------------------------------------
|
|
// TEST 3: NEW CODE — Config wipe during update, UUID survives via uuid-store
|
|
// --------------------------------------------------------------------------
|
|
console.log('\n--- Test 3: NEW CODE — Config wipe + UUID survives via uuid-store ---');
|
|
setup();
|
|
|
|
// Setup: player has UUID (stored in both config.json AND uuid-store.json)
|
|
const initialConfig = { username: 'SpecialK', userUuids: { 'SpecialK': 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' }, hasLaunchedBefore: true };
|
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(initialConfig, null, 2), 'utf8');
|
|
|
|
// First call migrates to uuid-store
|
|
const uuidFirst = getUuidForUser('SpecialK');
|
|
assertEqual(uuidFirst, 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'UUID correct before corruption');
|
|
assert(fs.existsSync(UUID_STORE_FILE), 'uuid-store.json created');
|
|
|
|
// Simulate: config.json gets wiped (same as the update bug)
|
|
fs.writeFileSync(CONFIG_FILE, '{}', 'utf8');
|
|
|
|
// Verify config is empty
|
|
const wipedConfig = loadConfig();
|
|
assert(!wipedConfig.userUuids || Object.keys(wipedConfig.userUuids).length === 0, 'Config wiped — no userUuids');
|
|
assert(!wipedConfig.username, 'Config wiped — no username');
|
|
|
|
// Player re-enters same name → UUID recovered from uuid-store!
|
|
const uuidAfterNew = getUuidForUser('SpecialK');
|
|
assertEqual(uuidAfterNew, 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'NEW CODE: UUID preserved after config wipe!');
|
|
|
|
// --------------------------------------------------------------------------
|
|
// TEST 4: saveConfig safety check — refuses to overwrite good data with empty
|
|
// --------------------------------------------------------------------------
|
|
console.log('\n--- Test 4: saveConfig safety check — blocks destructive writes ---');
|
|
setup();
|
|
|
|
// Setup: valid config file with data
|
|
const goodConfig = { username: 'SpecialK', userUuids: { 'SpecialK': 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' }, hasLaunchedBefore: true, installPath: 'C:\\Games\\Hytale' };
|
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(goodConfig, null, 2), 'utf8');
|
|
|
|
// Make the file temporarily unreadable by writing garbage (simulates file lock/corruption)
|
|
const originalContent = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
fs.writeFileSync(CONFIG_FILE, 'NOT VALID JSON!!!', 'utf8');
|
|
|
|
// Try to save — should refuse because file exists but can't be parsed
|
|
let saveThrew = false;
|
|
try {
|
|
saveConfig({ someNewField: true });
|
|
} catch (e) {
|
|
saveThrew = true;
|
|
}
|
|
|
|
// The file should still have the garbage (not overwritten with { someNewField: true })
|
|
const afterContent = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
|
|
// Restore original for backup recovery test
|
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(goodConfig, null, 2), 'utf8');
|
|
|
|
// Note: with invalid JSON, loadConfig returns {} and safety check triggers
|
|
// The save may eventually succeed on retry if the file becomes readable
|
|
// What matters is that it doesn't blindly overwrite
|
|
assert(afterContent !== '{\n "someNewField": true\n}', 'Safety check prevented blind overwrite of corrupted file');
|
|
|
|
// --------------------------------------------------------------------------
|
|
// TEST 5: Backup recovery — config.json corrupted, recovered from .bak
|
|
// --------------------------------------------------------------------------
|
|
console.log('\n--- Test 5: Backup recovery — auto-recover from .bak ---');
|
|
setup();
|
|
|
|
// Create config and backup
|
|
const validConfig = { username: 'SpecialK', userUuids: { 'SpecialK': 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' } };
|
|
fs.writeFileSync(CONFIG_BACKUP, JSON.stringify(validConfig, null, 2), 'utf8');
|
|
fs.writeFileSync(CONFIG_FILE, 'CORRUPTED', 'utf8');
|
|
|
|
const recovered = loadConfig();
|
|
assertEqual(recovered.username, 'SpecialK', 'Username recovered from backup');
|
|
assert(recovered.userUuids && recovered.userUuids['SpecialK'] === 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee', 'UUID recovered from backup');
|
|
|
|
// --------------------------------------------------------------------------
|
|
// TEST 6: Full update simulation — the exact scenario from player report
|
|
// --------------------------------------------------------------------------
|
|
console.log('\n--- Test 6: Full update simulation (player report scenario) ---');
|
|
setup();
|
|
|
|
// Step 1: Player installs v2.3.4, sets username, plays game
|
|
console.log(' Step 1: Player sets up profile...');
|
|
saveConfig({ username: 'Special K', hasLaunchedBefore: true });
|
|
const originalUuid = getUuidForUser('Special K');
|
|
console.log(` Original UUID: ${originalUuid}`);
|
|
|
|
// Step 2: v2.3.5 auto-update — new app launches
|
|
console.log(' Step 2: Simulating v2.3.5 update...');
|
|
|
|
// Simulate the 3 saveConfig calls that happen during startup
|
|
// But first, simulate config being temporarily locked (returns empty)
|
|
const preUpdateContent = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
fs.writeFileSync(CONFIG_FILE, '', 'utf8'); // Simulate: file empty during write (race condition)
|
|
|
|
// These are the 3 calls from: profileManager.init, migrateUserDataToCentralized, handleFirstLaunchCheck
|
|
// With our safety check, they should NOT wipe the data
|
|
try { saveConfig({ hasLaunchedBefore: true }); } catch (e) { /* expected — safety check blocks it */ }
|
|
|
|
// Simulate file becomes readable again (antivirus releases lock)
|
|
fs.writeFileSync(CONFIG_FILE, preUpdateContent, 'utf8');
|
|
|
|
// Step 3: Player re-enters username (because UI might show empty)
|
|
console.log(' Step 3: Player re-enters username...');
|
|
const postUpdateUuid = getUuidForUser('Special K');
|
|
console.log(` Post-update UUID: ${postUpdateUuid}`);
|
|
|
|
assertEqual(postUpdateUuid, originalUuid, 'UUID survived the full update cycle!');
|
|
|
|
// --------------------------------------------------------------------------
|
|
// TEST 7: Multiple users — UUIDs stay independent
|
|
// --------------------------------------------------------------------------
|
|
console.log('\n--- Test 7: Multiple users — UUIDs stay independent ---');
|
|
setup();
|
|
|
|
const uuidAlice = getUuidForUser('Alice');
|
|
const uuidBob = getUuidForUser('Bob');
|
|
const uuidCharlie = getUuidForUser('Charlie');
|
|
|
|
assert(uuidAlice !== uuidBob, 'Alice and Bob have different UUIDs');
|
|
assert(uuidBob !== uuidCharlie, 'Bob and Charlie have different UUIDs');
|
|
|
|
// Wipe config, all should survive
|
|
fs.writeFileSync(CONFIG_FILE, '{}', 'utf8');
|
|
|
|
assertEqual(getUuidForUser('Alice'), uuidAlice, 'Alice UUID survived config wipe');
|
|
assertEqual(getUuidForUser('Bob'), uuidBob, 'Bob UUID survived config wipe');
|
|
assertEqual(getUuidForUser('Charlie'), uuidCharlie, 'Charlie UUID survived config wipe');
|
|
|
|
// --------------------------------------------------------------------------
|
|
// TEST 8: UUID store deleted — recovery from config.json
|
|
// --------------------------------------------------------------------------
|
|
console.log('\n--- Test 8: UUID store deleted — recovery from config.json ---');
|
|
setup();
|
|
|
|
// Create UUID via normal flow (saves to both stores)
|
|
const uuidOriginal = getUuidForUser('TestPlayer');
|
|
|
|
// Delete uuid-store.json (simulates user manually deleting it or disk issue)
|
|
fs.unlinkSync(UUID_STORE_FILE);
|
|
assert(!fs.existsSync(UUID_STORE_FILE), 'uuid-store.json deleted');
|
|
|
|
// UUID should be recovered from config.json
|
|
const uuidRecovered = getUuidForUser('TestPlayer');
|
|
assertEqual(uuidRecovered, uuidOriginal, 'UUID recovered from config.json after uuid-store deletion');
|
|
assert(fs.existsSync(UUID_STORE_FILE), 'uuid-store.json recreated after recovery');
|
|
|
|
// --------------------------------------------------------------------------
|
|
// TEST 9: Both stores deleted — new UUID generated (fresh install)
|
|
// --------------------------------------------------------------------------
|
|
console.log('\n--- Test 9: Both stores deleted — new UUID (fresh install) ---');
|
|
setup();
|
|
|
|
const uuidFresh = getUuidForUser('NewPlayer');
|
|
|
|
// Delete both
|
|
fs.unlinkSync(UUID_STORE_FILE);
|
|
fs.unlinkSync(CONFIG_FILE);
|
|
|
|
const uuidAfterWipe = getUuidForUser('NewPlayer');
|
|
assert(uuidAfterWipe !== uuidFresh, 'New UUID generated when both stores are gone (expected for true fresh install)');
|
|
|
|
// --------------------------------------------------------------------------
|
|
// TEST 10: Worst case — config.json wiped AND uuid-store.json exists
|
|
// Simulates the EXACT player-reported scenario with new code
|
|
// --------------------------------------------------------------------------
|
|
console.log('\n--- Test 10: Exact player scenario with new code ---');
|
|
setup();
|
|
|
|
// Player has been playing for a while
|
|
saveConfig({
|
|
username: 'Special K',
|
|
hasLaunchedBefore: true,
|
|
installPath: 'C:\\Games\\Hytale',
|
|
version_client: '2026.02.19-1a311a592',
|
|
version_branch: 'release',
|
|
userUuids: { 'Special K': '11111111-2222-4333-9444-555555555555' }
|
|
});
|
|
|
|
// First call creates uuid-store.json
|
|
const originalUuid10 = getUuidForUser('Special K');
|
|
assertEqual(originalUuid10, '11111111-2222-4333-9444-555555555555', 'Original UUID loaded');
|
|
|
|
// BOOM: Update happens, config.json completely wiped
|
|
fs.writeFileSync(CONFIG_FILE, '{}', 'utf8');
|
|
|
|
// Username lost — player has to re-enter
|
|
const loadedUsername = loadConfig().username;
|
|
assert(!loadedUsername, 'Username is gone from config (simulating what player saw)');
|
|
|
|
// Player types "Special K" again in settings
|
|
saveConfig({ username: 'Special K' });
|
|
|
|
// Player clicks Play — getUuidForUser called
|
|
const recoveredUuid10 = getUuidForUser('Special K');
|
|
assertEqual(recoveredUuid10, '11111111-2222-4333-9444-555555555555', 'UUID recovered — character data preserved!');
|
|
|
|
// ============================================================================
|
|
// RESULTS
|
|
// ============================================================================
|
|
console.log('\n' + '='.repeat(70));
|
|
console.log(`RESULTS: ${passed} passed, ${failed} failed`);
|
|
if (failed > 0) {
|
|
console.log('\nFailures:');
|
|
failures.forEach(f => console.log(` ✗ ${f}`));
|
|
}
|
|
console.log('='.repeat(70));
|
|
|
|
cleanup();
|
|
process.exit(failed > 0 ? 1 : 0);
|