Profile System & Mod Loading Fixes

Added a full profile system and fixed a few critical mod loading issues.

What changed

Profiles — Implemented proper profile management (create, switch, delete). Each profile now has its own isolated mod list.

Mod Isolation — Fixed ModManager so mods are strictly scoped to the active profile. Browsing and installing only affects the selected profile.

Critical Fix — Fixed a path bug where mods were being saved to ~/AppData/Local on macOS (Windows path) instead of ~/Library/Application Support. Mods now save to the correct location and load correctly in-game.

Stability — Added an auto-sync step before every launch to make sure the physical mods folder always matches the active profile.

UI — Added a profile selector dropdown and a profile management modal.
This commit is contained in:
Rahul-Sahani04
2026-01-20 11:52:36 +05:30
parent fffc730788
commit 64892c81e9
14 changed files with 1424 additions and 509 deletions

View File

@@ -14,6 +14,10 @@ function getAuthDomain() {
}
// 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;
}
@@ -111,6 +115,16 @@ function saveJavaPath(javaPath) {
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 || '';
}
@@ -135,14 +149,29 @@ function loadDiscordRPC() {
function saveModsToConfig(mods) {
try {
let config = loadConfig();
config.installedMods = mods;
const config = loadConfig();
// Config migration to profiles handles the structure,
// but here we ensure that if we are saving mods, we save them to the ACTIVE profile
// OR we save to the global pool if that's the design.
// Based on the plan: Profile has "Enabled Mods".
// This function seems to be used to save the list of *Installed* mods.
// We'll update the global config for "installedMods" mostly for reference/migration,
// but primarily we should be updating the active profile's mod list.
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;
}
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');
} catch (error) {
@@ -153,6 +182,12 @@ function saveModsToConfig(mods) {
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);
@@ -162,19 +197,19 @@ function loadModsFromConfig() {
function isFirstLaunch() {
const config = loadConfig();
if ('hasLaunchedBefore' in config) {
return !config.hasLaunchedBefore;
}
const hasUserData = config.installPath || config.username || config.javaPath ||
config.chatUsername || config.userUuids ||
Object.keys(config).length > 0;
const hasUserData = config.installPath || config.username || config.javaPath ||
config.chatUsername || config.userUuids ||
Object.keys(config).length > 0;
if (!hasUserData) {
return true;
}
return true;
}
@@ -195,17 +230,17 @@ function getAllUuidMappings() {
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;
}
@@ -217,13 +252,13 @@ function generateNewUuid() {
function deleteUuidForUser(username) {
const config = loadConfig();
const userUuids = config.userUuids || {};
if (userUuids[username]) {
delete userUuids[username];
saveConfig({ userUuids });
return true;
}
return false;
}
@@ -231,7 +266,7 @@ function resetCurrentUserUuid() {
const username = loadUsername();
const { v4: uuidv4 } = require('uuid');
const newUuid = uuidv4();
return setUuidForUser(username, newUuid);
}

View File

@@ -77,32 +77,32 @@ function findClientPath(gameLatest) {
function findUserDataPath(gameLatest) {
const candidates = [];
candidates.push(path.join(gameLatest, 'Client', 'UserData'));
candidates.push(path.join(gameLatest, 'Client', 'Hytale.app', 'Contents', 'UserData'));
candidates.push(path.join(gameLatest, 'Hytale.app', 'Contents', 'UserData'));
candidates.push(path.join(gameLatest, 'UserData'));
candidates.push(path.join(gameLatest, 'Client', 'UserData'));
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
let defaultPath;
if (process.platform === 'darwin') {
defaultPath = path.join(gameLatest, 'Client', 'UserData');
} else {
defaultPath = path.join(gameLatest, 'Client', 'UserData');
}
if (!fs.existsSync(defaultPath)) {
fs.mkdirSync(defaultPath, { recursive: true });
}
return defaultPath;
}
@@ -110,15 +110,15 @@ function findUserDataRecursive(gameLatest) {
function searchDirectory(dir) {
try {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
if (item.isDirectory()) {
const fullPath = path.join(dir, item.name);
if (item.name === 'UserData') {
return fullPath;
}
const found = searchDirectory(fullPath);
if (found) {
return found;
@@ -127,14 +127,14 @@ function findUserDataRecursive(gameLatest) {
}
} catch (error) {
}
return null;
}
if (!fs.existsSync(gameLatest)) {
return null;
}
const found = searchDirectory(gameLatest);
return found;
}
@@ -152,16 +152,14 @@ async function getModsPath(customInstallPath = null) {
}
if (!installPath) {
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
installPath = path.join(localAppData, 'HytaleF2P');
} else {
installPath = path.join(installPath, 'HytaleF2P');
// Use the standard app directory logic which handles platforms correctly
installPath = getAppDir();
}
const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest');
const userDataPath = findUserDataPath(gameLatest);
const modsPath = path.join(userDataPath, 'Mods');
const disabledModsPath = path.join(userDataPath, 'DisabledMods');