mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-25 22:31:46 -03:00
Replace the raw textarea script editor with a structured form for Java wrapper configuration. Users now manage two lists (JVM flags to strip, args to inject with server/always condition) instead of editing bash/batch scripts directly. Scripts are generated at launch time from the structured config. Includes collapsible script preview for power users.
1144 lines
34 KiB
JavaScript
1144 lines
34 KiB
JavaScript
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';
|
|
|
|
// Get auth domain from env, config, or default
|
|
function getAuthDomain() {
|
|
// First check environment variable
|
|
if (process.env.HYTALE_AUTH_DOMAIN) {
|
|
return process.env.HYTALE_AUTH_DOMAIN;
|
|
}
|
|
// Then check config file
|
|
const config = loadConfig();
|
|
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
|
// Allow profile to override auth domain if ever needed
|
|
// but for now stick to global or env
|
|
}
|
|
if (config.authDomain) {
|
|
return config.authDomain;
|
|
}
|
|
// Fall back to default
|
|
return DEFAULT_AUTH_DOMAIN;
|
|
}
|
|
|
|
// Get full auth server URL
|
|
// Domain already includes subdomain (auth.sanasol.ws), so use directly
|
|
function getAuthServerUrl() {
|
|
const domain = getAuthDomain();
|
|
return `https://${domain}`;
|
|
}
|
|
|
|
// Save auth domain to config
|
|
function saveAuthDomain(domain) {
|
|
saveConfig({ authDomain: domain || DEFAULT_AUTH_DOMAIN });
|
|
}
|
|
|
|
function getAppDir() {
|
|
const home = os.homedir();
|
|
if (process.platform === 'win32') {
|
|
return path.join(home, 'AppData', 'Local', 'HytaleF2P');
|
|
} else if (process.platform === 'darwin') {
|
|
return path.join(home, 'Library', 'Application Support', 'HytaleF2P');
|
|
} else {
|
|
return path.join(home, '.hytalef2p');
|
|
}
|
|
}
|
|
|
|
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');
|
|
const UUID_STORE_FILE = path.join(getAppDir(), 'uuid-store.json');
|
|
|
|
// =============================================================================
|
|
// 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)) {
|
|
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 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) {
|
|
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();
|
|
|
|
// SAFETY: If config file exists on disk but loadConfig() returned empty,
|
|
// something is wrong (file locked, corrupted, etc.). Refuse to save
|
|
// because merging with {} would wipe all existing data (userUuids, username, etc.)
|
|
if (Object.keys(currentConfig).length === 0 && fs.existsSync(CONFIG_FILE)) {
|
|
const fileSize = fs.statSync(CONFIG_FILE).size;
|
|
if (fileSize > 2) { // More than just "{}"
|
|
console.error(`[Config] REFUSING to save — loaded empty but file exists (${fileSize} bytes). Retrying load...`);
|
|
// Wait and retry the load
|
|
const delay = attempt * 200;
|
|
const start = Date.now();
|
|
while (Date.now() - start < delay) { /* busy wait */ }
|
|
continue;
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
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();
|
|
|
|
// Also update UUID store (source of truth)
|
|
migrateUuidStoreIfNeeded();
|
|
const uuidStore = loadUuidStore();
|
|
|
|
if (isRename) {
|
|
// Find the UUID for the current username
|
|
const currentKey = Object.keys(userUuids).find(
|
|
k => k.toLowerCase() === currentName.toLowerCase()
|
|
);
|
|
const currentStoreKey = Object.keys(uuidStore).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;
|
|
// Same in UUID store
|
|
if (currentStoreKey) delete uuidStore[currentStoreKey];
|
|
uuidStore[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;
|
|
// Same in UUID store
|
|
const storeKey = Object.keys(uuidStore).find(k => k.toLowerCase() === currentName.toLowerCase());
|
|
if (storeKey) {
|
|
delete uuidStore[storeKey];
|
|
uuidStore[newName] = uuid;
|
|
}
|
|
console.log(`[Config] Updated username case: "${currentKey}" → "${newName}"`);
|
|
}
|
|
}
|
|
|
|
// Save UUID store
|
|
saveUuidStore(uuidStore);
|
|
|
|
// Save both username and updated userUuids to config
|
|
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();
|
|
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
|
|
// Uses separate uuid-store.json as source of truth (survives config.json corruption)
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Normalize username for UUID lookup (case-insensitive, trimmed)
|
|
*/
|
|
function normalizeUsername(username) {
|
|
if (!username || typeof username !== 'string') return null;
|
|
return username.trim().toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Load UUID store from separate file (independent of config.json)
|
|
*/
|
|
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) {
|
|
console.error('[UUID Store] Failed to load:', err.message);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
/**
|
|
* Save UUID store to separate file (atomic write)
|
|
*/
|
|
function saveUuidStore(store) {
|
|
try {
|
|
const dir = path.dirname(UUID_STORE_FILE);
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
const tmpFile = UUID_STORE_FILE + '.tmp';
|
|
fs.writeFileSync(tmpFile, JSON.stringify(store, null, 2), 'utf8');
|
|
fs.renameSync(tmpFile, UUID_STORE_FILE);
|
|
} catch (err) {
|
|
console.error('[UUID Store] Failed to save:', err.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* One-time migration: copy userUuids from config.json to uuid-store.json
|
|
*/
|
|
function migrateUuidStoreIfNeeded() {
|
|
if (fs.existsSync(UUID_STORE_FILE)) return; // Already migrated
|
|
const config = loadConfig();
|
|
if (config.userUuids && Object.keys(config.userUuids).length > 0) {
|
|
console.log('[UUID Store] Migrating', Object.keys(config.userUuids).length, 'UUIDs from config.json');
|
|
saveUuidStore(config.userUuids);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get UUID for a username
|
|
* Source of truth: uuid-store.json (separate from config.json)
|
|
* Also writes to config.json for backward compatibility
|
|
* Creates new UUID only if user doesn't exist in EITHER store
|
|
*/
|
|
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();
|
|
|
|
// Ensure UUID store exists (one-time migration from config.json)
|
|
migrateUuidStoreIfNeeded();
|
|
|
|
// 1. Check UUID store first (source of truth)
|
|
const uuidStore = loadUuidStore();
|
|
const storeKey = Object.keys(uuidStore).find(k => k.toLowerCase() === normalizedLookup);
|
|
|
|
if (storeKey) {
|
|
const existingUuid = uuidStore[storeKey];
|
|
|
|
// Update case if needed
|
|
if (storeKey !== displayName) {
|
|
console.log(`[UUID Store] Updating username case: "${storeKey}" → "${displayName}"`);
|
|
delete uuidStore[storeKey];
|
|
uuidStore[displayName] = existingUuid;
|
|
saveUuidStore(uuidStore);
|
|
}
|
|
|
|
// Sync to config.json (backward compat, 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) {
|
|
// Non-critical — UUID store is the source of truth
|
|
}
|
|
|
|
console.log(`[UUID] ${displayName} → ${existingUuid} (from uuid-store)`);
|
|
return existingUuid;
|
|
}
|
|
|
|
// 2. Fallback: check config.json (recovery if uuid-store.json was lost)
|
|
const config = loadConfig();
|
|
const userUuids = config.userUuids || {};
|
|
const configKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
|
|
|
if (configKey) {
|
|
const recoveredUuid = userUuids[configKey];
|
|
console.warn(`[UUID] RECOVERED "${displayName}" → ${recoveredUuid} from config.json (uuid-store was missing)`);
|
|
|
|
// Save to UUID store
|
|
uuidStore[displayName] = recoveredUuid;
|
|
saveUuidStore(uuidStore);
|
|
|
|
return recoveredUuid;
|
|
}
|
|
|
|
// 3. New user — generate UUID, save to BOTH stores
|
|
const newUuid = uuidv4();
|
|
console.log(`[UUID] NEW user "${displayName}" → ${newUuid}`);
|
|
|
|
// Save to UUID store (source of truth)
|
|
uuidStore[displayName] = newUuid;
|
|
saveUuidStore(uuidStore);
|
|
|
|
// Save to config.json (backward compat)
|
|
userUuids[displayName] = newUuid;
|
|
saveConfig({ userUuids });
|
|
|
|
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() {
|
|
migrateUuidStoreIfNeeded();
|
|
const uuidStore = loadUuidStore();
|
|
// Fallback to config if uuid-store is empty
|
|
if (Object.keys(uuidStore).length === 0) {
|
|
const config = loadConfig();
|
|
return config.userUuids || {};
|
|
}
|
|
return uuidStore;
|
|
}
|
|
|
|
/**
|
|
* Get all UUID mappings as array with current user flag
|
|
*/
|
|
function getAllUuidMappingsArray() {
|
|
const allMappings = getAllUuidMappings();
|
|
const currentUsername = loadUsername();
|
|
const normalizedCurrent = currentUsername ? currentUsername.toLowerCase() : null;
|
|
|
|
return Object.entries(allMappings).map(([username, uuid]) => ({
|
|
username,
|
|
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();
|
|
|
|
// 1. Update UUID store (source of truth)
|
|
migrateUuidStoreIfNeeded();
|
|
const uuidStore = loadUuidStore();
|
|
const storeKey = Object.keys(uuidStore).find(k => k.toLowerCase() === normalizedLookup);
|
|
if (storeKey) delete uuidStore[storeKey];
|
|
uuidStore[displayName] = uuid;
|
|
saveUuidStore(uuidStore);
|
|
|
|
// 2. Update config.json (backward compat)
|
|
const config = loadConfig();
|
|
const userUuids = config.userUuids || {};
|
|
const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
|
if (existingKey) delete userUuids[existingKey];
|
|
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();
|
|
let deleted = false;
|
|
|
|
// 1. Delete from UUID store (source of truth)
|
|
migrateUuidStoreIfNeeded();
|
|
const uuidStore = loadUuidStore();
|
|
const storeKey = Object.keys(uuidStore).find(k => k.toLowerCase() === normalizedLookup);
|
|
if (storeKey) {
|
|
delete uuidStore[storeKey];
|
|
saveUuidStore(uuidStore);
|
|
deleted = true;
|
|
}
|
|
|
|
// 2. Delete from config.json (backward compat)
|
|
const config = loadConfig();
|
|
const userUuids = config.userUuids || {};
|
|
const existingKey = Object.keys(userUuids).find(k => k.toLowerCase() === normalizedLookup);
|
|
if (existingKey) {
|
|
delete userUuids[existingKey];
|
|
saveConfig({ userUuids });
|
|
deleted = true;
|
|
}
|
|
|
|
if (deleted) console.log(`[Config] UUID deleted for "${username}"`);
|
|
return deleted;
|
|
}
|
|
|
|
/**
|
|
* 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 });
|
|
}
|
|
|
|
function loadJavaPath() {
|
|
const config = loadConfig();
|
|
|
|
// Prefer Active Profile's Java Path
|
|
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
|
const profile = config.profiles[config.activeProfileId];
|
|
if (profile.javaPath && profile.javaPath.trim().length > 0) {
|
|
return profile.javaPath;
|
|
}
|
|
}
|
|
|
|
// Fallback to global setting
|
|
return config.javaPath || '';
|
|
}
|
|
|
|
// =============================================================================
|
|
// INSTALL PATH MANAGEMENT
|
|
// =============================================================================
|
|
|
|
function saveInstallPath(installPath) {
|
|
const trimmed = (installPath || '').trim();
|
|
saveConfig({ installPath: trimmed });
|
|
}
|
|
|
|
function loadInstallPath() {
|
|
const config = loadConfig();
|
|
return config.installPath || '';
|
|
}
|
|
|
|
// =============================================================================
|
|
// DISCORD RPC SETTINGS
|
|
// =============================================================================
|
|
|
|
function saveDiscordRPC(enabled) {
|
|
saveConfig({ discordRPC: !!enabled });
|
|
}
|
|
|
|
function loadDiscordRPC() {
|
|
const config = loadConfig();
|
|
return config.discordRPC !== undefined ? config.discordRPC : true;
|
|
}
|
|
|
|
// =============================================================================
|
|
// LANGUAGE SETTINGS
|
|
// =============================================================================
|
|
|
|
function saveLanguage(language) {
|
|
saveConfig({ language: language || 'en' });
|
|
}
|
|
|
|
function loadLanguage() {
|
|
const config = loadConfig();
|
|
return config.language || 'en';
|
|
}
|
|
|
|
// =============================================================================
|
|
// LAUNCHER SETTINGS
|
|
// =============================================================================
|
|
|
|
function saveCloseLauncherOnStart(enabled) {
|
|
saveConfig({ closeLauncherOnStart: !!enabled });
|
|
}
|
|
|
|
function loadCloseLauncherOnStart() {
|
|
const config = loadConfig();
|
|
return config.closeLauncherOnStart !== undefined ? config.closeLauncherOnStart : false;
|
|
}
|
|
|
|
function saveLauncherHardwareAcceleration(enabled) {
|
|
saveConfig({ launcherHardwareAcceleration: !!enabled });
|
|
}
|
|
|
|
function loadLauncherHardwareAcceleration() {
|
|
const config = loadConfig();
|
|
return config.launcherHardwareAcceleration !== undefined ? config.launcherHardwareAcceleration : true;
|
|
}
|
|
|
|
// =============================================================================
|
|
// MODS MANAGEMENT
|
|
// =============================================================================
|
|
|
|
function saveModsToConfig(mods) {
|
|
try {
|
|
const config = loadConfig();
|
|
|
|
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
|
config.profiles[config.activeProfileId].mods = mods;
|
|
} else {
|
|
config.installedMods = mods;
|
|
}
|
|
|
|
// Use atomic save
|
|
const configDir = path.dirname(CONFIG_FILE);
|
|
if (!fs.existsSync(configDir)) {
|
|
fs.mkdirSync(configDir, { recursive: true });
|
|
}
|
|
|
|
// 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('[Config] Error saving mods:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function loadModsFromConfig() {
|
|
try {
|
|
const config = loadConfig();
|
|
|
|
if (config.activeProfileId && config.profiles && config.profiles[config.activeProfileId]) {
|
|
return config.profiles[config.activeProfileId].mods || [];
|
|
}
|
|
|
|
return config.installedMods || [];
|
|
} catch (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;
|
|
|
|
if (!hasUserData) {
|
|
return true;
|
|
}
|
|
|
|
// FIXED: Was returning true here, should be false
|
|
return false;
|
|
}
|
|
|
|
function markAsLaunched() {
|
|
saveConfig({ hasLaunchedBefore: true, firstLaunchDate: new Date().toISOString() });
|
|
}
|
|
|
|
// =============================================================================
|
|
// GPU PREFERENCE
|
|
// =============================================================================
|
|
|
|
function saveGpuPreference(gpuPreference) {
|
|
saveConfig({ gpuPreference: gpuPreference || 'auto' });
|
|
}
|
|
|
|
function loadGpuPreference() {
|
|
const config = loadConfig();
|
|
return config.gpuPreference || 'auto';
|
|
}
|
|
|
|
// =============================================================================
|
|
// VERSION MANAGEMENT
|
|
// =============================================================================
|
|
|
|
function saveVersionClient(versionClient) {
|
|
saveConfig({ version_client: versionClient });
|
|
}
|
|
|
|
function loadVersionClient() {
|
|
const config = loadConfig();
|
|
return config.version_client !== undefined ? config.version_client : null;
|
|
}
|
|
|
|
function saveVersionBranch(versionBranch) {
|
|
const branch = versionBranch || 'release';
|
|
if (branch !== 'release' && branch !== 'pre-release') {
|
|
console.warn(`[Config] Invalid branch "${branch}", defaulting to "release"`);
|
|
saveConfig({ version_branch: 'release' });
|
|
} else {
|
|
saveConfig({ version_branch: branch });
|
|
}
|
|
}
|
|
|
|
function loadVersionBranch() {
|
|
const config = loadConfig();
|
|
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
|
|
};
|
|
}
|
|
|
|
// =============================================================================
|
|
// JAVA WRAPPER CONFIGURATION (Structured)
|
|
// =============================================================================
|
|
|
|
const DEFAULT_WRAPPER_CONFIG = {
|
|
stripFlags: ['-XX:+UseCompactObjectHeaders'],
|
|
injectArgs: [
|
|
{ arg: '--disable-sentry', condition: 'server' }
|
|
]
|
|
};
|
|
|
|
function getDefaultWrapperConfig() {
|
|
return JSON.parse(JSON.stringify(DEFAULT_WRAPPER_CONFIG));
|
|
}
|
|
|
|
function loadWrapperConfig() {
|
|
const config = loadConfig();
|
|
if (config.javaWrapperConfig && typeof config.javaWrapperConfig === 'object') {
|
|
const wc = config.javaWrapperConfig;
|
|
if (Array.isArray(wc.stripFlags) && Array.isArray(wc.injectArgs)) {
|
|
const loaded = JSON.parse(JSON.stringify(wc));
|
|
// Normalize entries: ensure every injectArg has a valid condition
|
|
for (const entry of loaded.injectArgs) {
|
|
if (!['server', 'always'].includes(entry.condition)) {
|
|
entry.condition = 'always';
|
|
}
|
|
}
|
|
return loaded;
|
|
}
|
|
}
|
|
return getDefaultWrapperConfig();
|
|
}
|
|
|
|
function saveWrapperConfig(wrapperConfig) {
|
|
if (!wrapperConfig || typeof wrapperConfig !== 'object') {
|
|
throw new Error('Invalid wrapper config');
|
|
}
|
|
if (!Array.isArray(wrapperConfig.stripFlags) || !Array.isArray(wrapperConfig.injectArgs)) {
|
|
throw new Error('Invalid wrapper config structure');
|
|
}
|
|
// Validate injectArgs entries
|
|
for (const entry of wrapperConfig.injectArgs) {
|
|
if (!entry.arg || typeof entry.arg !== 'string') {
|
|
throw new Error('Each inject arg must have a string "arg" property');
|
|
}
|
|
if (!['server', 'always'].includes(entry.condition)) {
|
|
throw new Error('Inject arg condition must be "server" or "always"');
|
|
}
|
|
}
|
|
saveConfig({ javaWrapperConfig: wrapperConfig });
|
|
console.log('[Config] Wrapper config saved');
|
|
}
|
|
|
|
function resetWrapperConfig() {
|
|
const config = loadConfig();
|
|
delete config.javaWrapperConfig;
|
|
delete config.javaWrapperScripts; // Clean up legacy key if present
|
|
|
|
// Write the cleaned config using the same atomic pattern as saveConfig.
|
|
// We cannot use saveConfig() here because it merges (spread) which cannot remove keys.
|
|
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] Wrapper config reset to default');
|
|
return getDefaultWrapperConfig();
|
|
}
|
|
|
|
/**
|
|
* Generate a platform-specific wrapper script from structured config
|
|
* @param {Object} config - { stripFlags: string[], injectArgs: {arg, condition}[] }
|
|
* @param {string} platform - 'darwin', 'win32', or 'linux'
|
|
* @param {string|null} javaBin - Path to real java binary (required for darwin/linux)
|
|
* @returns {string} Generated script content
|
|
*/
|
|
function generateWrapperScript(config, platform, javaBin) {
|
|
const { stripFlags, injectArgs } = config;
|
|
const alwaysArgs = injectArgs.filter(a => a.condition === 'always');
|
|
const serverArgs = injectArgs.filter(a => a.condition === 'server');
|
|
|
|
if (platform === 'win32') {
|
|
return _generateWindowsWrapper(stripFlags, alwaysArgs, serverArgs);
|
|
} else {
|
|
return _generateUnixWrapper(stripFlags, alwaysArgs, serverArgs, javaBin);
|
|
}
|
|
}
|
|
|
|
function _generateUnixWrapper(stripFlags, alwaysArgs, serverArgs, javaBin) {
|
|
const lines = [
|
|
'#!/bin/bash',
|
|
'# Java wrapper - generated by HytaleF2P launcher',
|
|
`REAL_JAVA="${javaBin || '${JAVA_BIN}'}"`,
|
|
'ARGS=("$@")',
|
|
''
|
|
];
|
|
|
|
// Strip flags
|
|
if (stripFlags.length > 0) {
|
|
lines.push('# Strip JVM flags');
|
|
lines.push('FILTERED_ARGS=()');
|
|
lines.push('for arg in "${ARGS[@]}"; do');
|
|
lines.push(' case "$arg" in');
|
|
for (const flag of stripFlags) {
|
|
lines.push(` "${flag}") echo "[Wrapper] Stripped: $arg" ;;`);
|
|
}
|
|
lines.push(' *) FILTERED_ARGS+=("$arg") ;;');
|
|
lines.push(' esac');
|
|
lines.push('done');
|
|
} else {
|
|
lines.push('FILTERED_ARGS=("${ARGS[@]}")');
|
|
}
|
|
lines.push('');
|
|
|
|
// Always-inject args
|
|
if (alwaysArgs.length > 0) {
|
|
lines.push('# Inject args (always)');
|
|
for (const a of alwaysArgs) {
|
|
lines.push(`FILTERED_ARGS+=("${a.arg}")`);
|
|
lines.push(`echo "[Wrapper] Injected ${a.arg}"`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
|
|
// Server-conditional args (appended after HytaleServer.jar if present)
|
|
if (serverArgs.length > 0) {
|
|
lines.push('# Inject args (server only)');
|
|
lines.push('IS_SERVER=false');
|
|
lines.push('for arg in "${FILTERED_ARGS[@]}"; do');
|
|
lines.push(' if [[ "$arg" == *"HytaleServer.jar"* ]]; then');
|
|
lines.push(' IS_SERVER=true');
|
|
lines.push(' break');
|
|
lines.push(' fi');
|
|
lines.push('done');
|
|
lines.push('if [ "$IS_SERVER" = true ]; then');
|
|
for (const a of serverArgs) {
|
|
lines.push(` FILTERED_ARGS+=("${a.arg}")`);
|
|
lines.push(` echo "[Wrapper] Injected ${a.arg}"`);
|
|
}
|
|
lines.push('fi');
|
|
lines.push('');
|
|
}
|
|
|
|
lines.push('echo "[Wrapper] Executing: $REAL_JAVA ${FILTERED_ARGS[*]}"');
|
|
lines.push('exec "$REAL_JAVA" "${FILTERED_ARGS[@]}"');
|
|
lines.push('');
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function _generateWindowsWrapper(stripFlags, alwaysArgs, serverArgs) {
|
|
const lines = [
|
|
'@echo off',
|
|
'setlocal EnableDelayedExpansion',
|
|
'',
|
|
'REM Java wrapper - generated by HytaleF2P launcher',
|
|
'set "REAL_JAVA=%~dp0java-original.exe"',
|
|
'set "ARGS=%*"',
|
|
''
|
|
];
|
|
|
|
// Strip flags using string replacement
|
|
if (stripFlags.length > 0) {
|
|
lines.push('REM Strip JVM flags');
|
|
for (const flag of stripFlags) {
|
|
lines.push(`set "ARGS=!ARGS:${flag}=!"`);
|
|
}
|
|
lines.push('');
|
|
}
|
|
|
|
// Always-inject args
|
|
const alwaysExtra = alwaysArgs.map(a => a.arg).join(' ');
|
|
|
|
// Server-conditional args
|
|
if (serverArgs.length > 0) {
|
|
const serverExtra = serverArgs.map(a => a.arg).join(' ');
|
|
lines.push('REM Check if running HytaleServer.jar and inject server args');
|
|
lines.push('echo !ARGS! | findstr /i "HytaleServer.jar" >nul 2>&1');
|
|
lines.push('if "!ERRORLEVEL!"=="0" (');
|
|
if (alwaysExtra) {
|
|
lines.push(` echo [Wrapper] Injected ${alwaysExtra} ${serverExtra}`);
|
|
lines.push(` "%REAL_JAVA%" !ARGS! ${alwaysExtra} ${serverExtra}`);
|
|
} else {
|
|
lines.push(` echo [Wrapper] Injected ${serverExtra}`);
|
|
lines.push(` "%REAL_JAVA%" !ARGS! ${serverExtra}`);
|
|
}
|
|
lines.push(') else (');
|
|
if (alwaysExtra) {
|
|
lines.push(` "%REAL_JAVA%" !ARGS! ${alwaysExtra}`);
|
|
} else {
|
|
lines.push(' "%REAL_JAVA%" !ARGS!');
|
|
}
|
|
lines.push(')');
|
|
} else if (alwaysExtra) {
|
|
lines.push(`"%REAL_JAVA%" !ARGS! ${alwaysExtra}`);
|
|
} else {
|
|
lines.push('"%REAL_JAVA%" !ARGS!');
|
|
}
|
|
|
|
lines.push('exit /b !ERRORLEVEL!');
|
|
lines.push('');
|
|
|
|
return lines.join('\r\n');
|
|
}
|
|
|
|
// =============================================================================
|
|
// EXPORTS
|
|
// =============================================================================
|
|
|
|
module.exports = {
|
|
// Core config
|
|
loadConfig,
|
|
saveConfig,
|
|
validateConfig,
|
|
|
|
// Username (no silent fallbacks)
|
|
saveUsername,
|
|
loadUsername,
|
|
loadUsernameWithDefault,
|
|
hasUsername,
|
|
|
|
// UUID management
|
|
getUuidForUser,
|
|
getCurrentUuid,
|
|
getAllUuidMappings,
|
|
getAllUuidMappingsArray,
|
|
setUuidForUser,
|
|
generateNewUuid,
|
|
deleteUuidForUser,
|
|
resetCurrentUserUuid,
|
|
|
|
// Java/Install paths
|
|
saveJavaPath,
|
|
loadJavaPath,
|
|
saveInstallPath,
|
|
loadInstallPath,
|
|
|
|
// Settings
|
|
saveDiscordRPC,
|
|
loadDiscordRPC,
|
|
saveLanguage,
|
|
loadLanguage,
|
|
saveCloseLauncherOnStart,
|
|
loadCloseLauncherOnStart,
|
|
saveLauncherHardwareAcceleration,
|
|
loadLauncherHardwareAcceleration,
|
|
|
|
// Mods
|
|
saveModsToConfig,
|
|
loadModsFromConfig,
|
|
|
|
// Launch state
|
|
isFirstLaunch,
|
|
markAsLaunched,
|
|
checkLaunchReady,
|
|
|
|
// Auth server
|
|
getAuthServerUrl,
|
|
getAuthDomain,
|
|
saveAuthDomain,
|
|
|
|
// GPU
|
|
saveGpuPreference,
|
|
loadGpuPreference,
|
|
|
|
// Version
|
|
saveVersionClient,
|
|
loadVersionClient,
|
|
saveVersionBranch,
|
|
loadVersionBranch,
|
|
|
|
// Java Wrapper Config
|
|
getDefaultWrapperConfig,
|
|
loadWrapperConfig,
|
|
saveWrapperConfig,
|
|
resetWrapperConfig,
|
|
generateWrapperScript,
|
|
|
|
// Constants
|
|
CONFIG_FILE,
|
|
UUID_STORE_FILE
|
|
};
|