Files
hytale-f2p/backend/core/config.js

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: [],
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
};