mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 10:31:47 -03:00
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:
@@ -11,6 +11,7 @@ const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServ
|
||||
const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
|
||||
const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager');
|
||||
const { updateGameFiles } = require('./gameManager');
|
||||
const { syncModsForCurrentProfile } = require('./modManager');
|
||||
|
||||
// Client patcher for custom auth server (sanasol.ws)
|
||||
let clientPatcher = null;
|
||||
@@ -130,7 +131,7 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
||||
}
|
||||
} else {
|
||||
javaBin = getJavaExec(customJreDir);
|
||||
|
||||
|
||||
if (!getBundledJavaPath(customJreDir)) {
|
||||
const fallback = await detectSystemJava();
|
||||
if (fallback) {
|
||||
@@ -192,9 +193,9 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
||||
const serverDir = path.join(gameLatest, 'Server');
|
||||
|
||||
const signPath = async (targetPath, deep = false) => {
|
||||
await execAsync(`xattr -cr "${targetPath}"`).catch(() => {});
|
||||
await execAsync(`xattr -cr "${targetPath}"`).catch(() => { });
|
||||
const deepFlag = deep ? '--deep ' : '';
|
||||
await execAsync(`codesign --force ${deepFlag}--sign - "${targetPath}"`).catch(() => {});
|
||||
await execAsync(`codesign --force ${deepFlag}--sign - "${targetPath}"`).catch(() => { });
|
||||
};
|
||||
|
||||
if (fs.existsSync(appBundle)) {
|
||||
@@ -216,8 +217,8 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
|
||||
}
|
||||
|
||||
if (fs.existsSync(serverDir)) {
|
||||
await execAsync(`xattr -cr "${serverDir}"`).catch(() => {});
|
||||
await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => {});
|
||||
await execAsync(`xattr -cr "${serverDir}"`).catch(() => { });
|
||||
await execAsync(`find "${serverDir}" -type f -perm +111 -exec codesign --force --sign - {} \\;`).catch(() => { });
|
||||
console.log('Signed server binaries (after patching)');
|
||||
}
|
||||
|
||||
@@ -260,11 +261,23 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
if (progressCallback) {
|
||||
progressCallback('Starting game...', null, null, null, null);
|
||||
}
|
||||
|
||||
// Ensure mods are synced for the active profile before launching
|
||||
try {
|
||||
console.log('Syncing mods for active profile before launch...');
|
||||
if (progressCallback) progressCallback('Syncing mods...', null, null, null, null);
|
||||
await syncModsForCurrentProfile();
|
||||
} catch (syncError) {
|
||||
console.error('Failed to sync mods before launch:', syncError);
|
||||
// Continue anyway? Or fail?
|
||||
// Warn user but continue might be safer to avoid blocking play if sync is just glitchy
|
||||
}
|
||||
|
||||
console.log('Starting game...');
|
||||
console.log(`Command: "${clientPath}" ${args.join(' ')}`);
|
||||
|
||||
const env = { ...process.env };
|
||||
|
||||
|
||||
const waylandEnv = setupWaylandEnvironment();
|
||||
Object.assign(env, waylandEnv);
|
||||
|
||||
@@ -276,8 +289,8 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
|
||||
};
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
spawnOptions.shell = false;
|
||||
spawnOptions.windowsHide = true;
|
||||
spawnOptions.shell = false;
|
||||
spawnOptions.windowsHide = true;
|
||||
}
|
||||
|
||||
const child = spawn(clientPath, args, spawnOptions);
|
||||
@@ -371,12 +384,12 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
|
||||
try {
|
||||
await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir);
|
||||
console.log('Game updated successfully, waiting before launch...');
|
||||
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Preparing game launch...', 90, null, null, null);
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
} catch (updateError) {
|
||||
console.error('Update failed:', updateError);
|
||||
if (progressCallback) {
|
||||
|
||||
@@ -4,6 +4,7 @@ const crypto = require('crypto');
|
||||
const axios = require('axios');
|
||||
const { getModsPath } = require('../core/paths');
|
||||
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
|
||||
const profileManager = require('./profileManager');
|
||||
|
||||
function generateModId(filename) {
|
||||
return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8);
|
||||
@@ -11,13 +12,13 @@ function generateModId(filename) {
|
||||
|
||||
function extractModName(filename) {
|
||||
let name = path.parse(filename).name;
|
||||
|
||||
|
||||
name = name.replace(/-v?\d+\.[\d\.]+.*$/i, '');
|
||||
name = name.replace(/-\d+\.[\d\.]+.*$/i, '');
|
||||
|
||||
|
||||
name = name.replace(/[-_]/g, ' ');
|
||||
name = name.replace(/\b\w/g, l => l.toUpperCase());
|
||||
|
||||
|
||||
return name || 'Unknown Mod';
|
||||
}
|
||||
|
||||
@@ -26,77 +27,53 @@ function extractVersion(filename) {
|
||||
return versionMatch ? versionMatch[1] : null;
|
||||
}
|
||||
|
||||
// Helper to get mods from active profile
|
||||
function getProfileMods() {
|
||||
const profile = profileManager.getActiveProfile();
|
||||
return profile ? (profile.mods || []) : [];
|
||||
}
|
||||
|
||||
async function loadInstalledMods(modsPath) {
|
||||
try {
|
||||
const configMods = loadModsFromConfig();
|
||||
const modsMap = new Map();
|
||||
|
||||
configMods.forEach(mod => {
|
||||
modsMap.set(mod.fileName, mod);
|
||||
});
|
||||
|
||||
if (fs.existsSync(modsPath)) {
|
||||
const files = fs.readdirSync(modsPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(modsPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
if (stats.isFile() && (file.endsWith('.jar') || file.endsWith('.zip'))) {
|
||||
const configMod = modsMap.get(file);
|
||||
|
||||
const modInfo = {
|
||||
id: configMod?.id || generateModId(file),
|
||||
name: configMod?.name || extractModName(file),
|
||||
version: configMod?.version || extractVersion(file) || '1.0.0',
|
||||
description: configMod?.description || 'Installed mod',
|
||||
author: configMod?.author || 'Unknown',
|
||||
enabled: true,
|
||||
filePath: filePath,
|
||||
fileName: file,
|
||||
fileSize: configMod?.fileSize || stats.size,
|
||||
dateInstalled: configMod?.dateInstalled || stats.birthtime || stats.mtime,
|
||||
curseForgeId: configMod?.curseForgeId,
|
||||
curseForgeFileId: configMod?.curseForgeFileId
|
||||
};
|
||||
|
||||
modsMap.set(file, modInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const activeProfile = profileManager.getActiveProfile();
|
||||
if (!activeProfile) return [];
|
||||
|
||||
const profileMods = activeProfile.mods || [];
|
||||
const profileModFiles = new Set(profileMods.map(m => m.fileName));
|
||||
|
||||
// We only return mods that are explicitly in the profile
|
||||
// Check which ones are physically present (either in mods/ or DisabledMods/)
|
||||
|
||||
const physicalModsPath = modsPath; // .../mods
|
||||
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
||||
if (fs.existsSync(disabledModsPath)) {
|
||||
const files = fs.readdirSync(disabledModsPath);
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(disabledModsPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
if (stats.isFile() && (file.endsWith('.jar') || file.endsWith('.zip'))) {
|
||||
const configMod = modsMap.get(file);
|
||||
|
||||
const modInfo = {
|
||||
id: configMod?.id || generateModId(file),
|
||||
name: configMod?.name || extractModName(file),
|
||||
version: configMod?.version || extractVersion(file) || '1.0.0',
|
||||
description: configMod?.description || 'Disabled mod',
|
||||
author: configMod?.author || 'Unknown',
|
||||
enabled: false,
|
||||
filePath: filePath,
|
||||
fileName: file,
|
||||
fileSize: configMod?.fileSize || stats.size,
|
||||
dateInstalled: configMod?.dateInstalled || stats.birthtime || stats.mtime,
|
||||
curseForgeId: configMod?.curseForgeId,
|
||||
curseForgeFileId: configMod?.curseForgeFileId
|
||||
};
|
||||
|
||||
modsMap.set(file, modInfo);
|
||||
}
|
||||
|
||||
const validMods = [];
|
||||
|
||||
for (const modConfig of profileMods) {
|
||||
// Check if file exists in either location
|
||||
const inEnabled = fs.existsSync(path.join(physicalModsPath, modConfig.fileName));
|
||||
const inDisabled = fs.existsSync(path.join(disabledModsPath, modConfig.fileName));
|
||||
|
||||
if (inEnabled || inDisabled) {
|
||||
validMods.push({
|
||||
...modConfig,
|
||||
// Set filePath based on physical location
|
||||
filePath: inEnabled ? path.join(physicalModsPath, modConfig.fileName) : path.join(disabledModsPath, modConfig.fileName),
|
||||
enabled: modConfig.enabled !== false // Default true
|
||||
});
|
||||
} else {
|
||||
console.warn(`[ModManager] Mod ${modConfig.fileName} listed in profile but not found on disk.`);
|
||||
// Include it so user can see it's missing or remove it
|
||||
validMods.push({
|
||||
...modConfig,
|
||||
filePath: null,
|
||||
missing: true,
|
||||
enabled: modConfig.enabled !== false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(modsMap.values());
|
||||
|
||||
return validMods;
|
||||
} catch (error) {
|
||||
console.error('Error loading installed mods:', error);
|
||||
return [];
|
||||
@@ -106,13 +83,13 @@ async function loadInstalledMods(modsPath) {
|
||||
async function downloadMod(modInfo) {
|
||||
try {
|
||||
const modsPath = await getModsPath();
|
||||
|
||||
|
||||
if (!modInfo.downloadUrl && !modInfo.fileId) {
|
||||
throw new Error('No download URL or file ID provided');
|
||||
}
|
||||
|
||||
|
||||
let downloadUrl = modInfo.downloadUrl;
|
||||
|
||||
|
||||
if (!downloadUrl && modInfo.fileId && modInfo.modId) {
|
||||
const response = await axios.get(`https://api.curseforge.com/v1/mods/${modInfo.modId}/files/${modInfo.fileId}`, {
|
||||
headers: {
|
||||
@@ -120,56 +97,61 @@ async function downloadMod(modInfo) {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
downloadUrl = response.data.data.downloadUrl;
|
||||
}
|
||||
|
||||
|
||||
if (!downloadUrl) {
|
||||
throw new Error('Could not determine download URL');
|
||||
}
|
||||
|
||||
|
||||
const fileName = modInfo.fileName || `mod-${modInfo.modId}.jar`;
|
||||
const filePath = path.join(modsPath, fileName);
|
||||
|
||||
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: downloadUrl,
|
||||
responseType: 'stream'
|
||||
});
|
||||
|
||||
|
||||
const writer = fs.createWriteStream(filePath);
|
||||
response.data.pipe(writer);
|
||||
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', () => {
|
||||
const configMods = loadModsFromConfig();
|
||||
const newMod = {
|
||||
id: modInfo.id || generateModId(fileName),
|
||||
name: modInfo.name || extractModName(fileName),
|
||||
version: modInfo.version || '1.0.0',
|
||||
description: modInfo.summary || modInfo.description || 'Downloaded from CurseForge',
|
||||
author: modInfo.author || 'Unknown',
|
||||
enabled: true,
|
||||
fileName: fileName,
|
||||
fileSize: fs.statSync(filePath).size,
|
||||
dateInstalled: new Date().toISOString(),
|
||||
curseForgeId: modInfo.modId,
|
||||
curseForgeFileId: modInfo.fileId
|
||||
};
|
||||
|
||||
configMods.push(newMod);
|
||||
saveModsToConfig(configMods);
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
modInfo: newMod
|
||||
});
|
||||
// NEW: Update Active Profile instead of global config
|
||||
const activeProfile = profileManager.getActiveProfile();
|
||||
if (activeProfile) {
|
||||
const newMod = {
|
||||
id: modInfo.id || generateModId(fileName),
|
||||
name: modInfo.name || extractModName(fileName),
|
||||
version: modInfo.version || '1.0.0',
|
||||
description: modInfo.summary || modInfo.description || 'Downloaded from CurseForge',
|
||||
author: modInfo.author || 'Unknown',
|
||||
enabled: true,
|
||||
fileName: fileName,
|
||||
fileSize: fs.statSync(filePath).size,
|
||||
dateInstalled: new Date().toISOString(),
|
||||
curseForgeId: modInfo.modId,
|
||||
curseForgeFileId: modInfo.fileId
|
||||
};
|
||||
|
||||
const updatedMods = [...(activeProfile.mods || []), newMod];
|
||||
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
filePath: filePath,
|
||||
fileName: fileName,
|
||||
modInfo: newMod
|
||||
});
|
||||
} else {
|
||||
reject(new Error('No active profile to save mod to'));
|
||||
}
|
||||
});
|
||||
writer.on('error', reject);
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error downloading mod:', error);
|
||||
return {
|
||||
@@ -181,36 +163,39 @@ async function downloadMod(modInfo) {
|
||||
|
||||
async function uninstallMod(modId, modsPath) {
|
||||
try {
|
||||
const configMods = loadModsFromConfig();
|
||||
const mod = configMods.find(m => m.id === modId);
|
||||
|
||||
const activeProfile = profileManager.getActiveProfile();
|
||||
if (!activeProfile) throw new Error('No active profile');
|
||||
|
||||
const profileMods = activeProfile.mods || [];
|
||||
const mod = profileMods.find(m => m.id === modId);
|
||||
|
||||
if (!mod) {
|
||||
throw new Error('Mod not found in config');
|
||||
throw new Error('Mod not found in profile');
|
||||
}
|
||||
|
||||
|
||||
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
||||
const enabledPath = path.join(modsPath, mod.fileName);
|
||||
const disabledPath = path.join(disabledModsPath, mod.fileName);
|
||||
|
||||
|
||||
let fileRemoved = false;
|
||||
// Try to remove file from both locations to be safe
|
||||
if (fs.existsSync(enabledPath)) {
|
||||
fs.unlinkSync(enabledPath);
|
||||
fileRemoved = true;
|
||||
console.log('Removed mod from Mods folder:', enabledPath);
|
||||
} else if (fs.existsSync(disabledPath)) {
|
||||
fs.unlinkSync(disabledPath);
|
||||
fileRemoved = true;
|
||||
console.log('Removed mod from DisabledMods folder:', disabledPath);
|
||||
}
|
||||
|
||||
if (fs.existsSync(disabledPath)) {
|
||||
try { fs.unlinkSync(disabledPath); fileRemoved = true; } catch (e) { }
|
||||
}
|
||||
|
||||
if (!fileRemoved) {
|
||||
console.warn('Mod file not found on filesystem, removing from config anyway');
|
||||
console.warn('Mod file not found on filesystem, removing from profile anyway');
|
||||
}
|
||||
|
||||
const updatedMods = configMods.filter(m => m.id !== modId);
|
||||
saveModsToConfig(updatedMods);
|
||||
console.log('Mod removed from config.json');
|
||||
|
||||
|
||||
const updatedMods = profileMods.filter(m => m.id !== modId);
|
||||
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
|
||||
|
||||
console.log('Mod removed from profile');
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error uninstalling mod:', error);
|
||||
@@ -223,38 +208,53 @@ async function uninstallMod(modId, modsPath) {
|
||||
|
||||
async function toggleMod(modId, modsPath) {
|
||||
try {
|
||||
const mods = await loadInstalledMods(modsPath);
|
||||
const mod = mods.find(m => m.id === modId);
|
||||
|
||||
if (!mod) {
|
||||
throw new Error('Mod not found');
|
||||
const activeProfile = profileManager.getActiveProfile();
|
||||
if (!activeProfile) throw new Error('No active profile');
|
||||
|
||||
const profileMods = activeProfile.mods || [];
|
||||
const modIndex = profileMods.findIndex(m => m.id === modId);
|
||||
|
||||
if (modIndex === -1) {
|
||||
throw new Error('Mod not found in profile');
|
||||
}
|
||||
|
||||
const mod = profileMods[modIndex];
|
||||
const newEnabled = !mod.enabled; // Toggle
|
||||
|
||||
// Update Profile First
|
||||
const updatedMods = [...profileMods];
|
||||
updatedMods[modIndex] = { ...mod, enabled: newEnabled };
|
||||
profileManager.updateProfile(activeProfile.id, { mods: updatedMods });
|
||||
|
||||
// Manually move the file to reflect the new state
|
||||
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
||||
if (!fs.existsSync(disabledModsPath)) {
|
||||
fs.mkdirSync(disabledModsPath, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(disabledModsPath)) fs.mkdirSync(disabledModsPath, { recursive: true });
|
||||
|
||||
const currentPath = mod.filePath;
|
||||
let newPath, newEnabled;
|
||||
const currentPath = mod.enabled ? path.join(modsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName);
|
||||
|
||||
if (mod.enabled) {
|
||||
newPath = path.join(disabledModsPath, path.basename(currentPath));
|
||||
newEnabled = false;
|
||||
// Determine target paths
|
||||
|
||||
const targetDir = newEnabled ? modsPath : disabledModsPath;
|
||||
const targetPath = path.join(targetDir, mod.fileName);
|
||||
|
||||
if (fs.existsSync(currentPath)) {
|
||||
fs.renameSync(currentPath, targetPath);
|
||||
} else {
|
||||
newPath = path.join(modsPath, path.basename(currentPath));
|
||||
newEnabled = true;
|
||||
// Fallback: check if it's already in target?
|
||||
|
||||
|
||||
if (fs.existsSync(targetPath)) {
|
||||
// It's already there, maybe just state was wrong.
|
||||
|
||||
console.log(`[ModManager] Mod ${mod.fileName} is already in the correct state`);
|
||||
|
||||
} else {
|
||||
// Try finding it
|
||||
const altPath = mod.enabled ? path.join(disabledModsPath, mod.fileName) : path.join(modsPath, mod.fileName);
|
||||
if (fs.existsSync(altPath)) fs.renameSync(altPath, targetPath);
|
||||
}
|
||||
}
|
||||
|
||||
fs.renameSync(currentPath, newPath);
|
||||
|
||||
const configMods = loadModsFromConfig();
|
||||
const configModIndex = configMods.findIndex(m => m.id === modId);
|
||||
if (configModIndex !== -1) {
|
||||
configMods[configModIndex].enabled = newEnabled;
|
||||
saveModsToConfig(configMods);
|
||||
}
|
||||
|
||||
return { success: true, enabled: newEnabled };
|
||||
} catch (error) {
|
||||
console.error('Error toggling mod:', error);
|
||||
@@ -265,11 +265,69 @@ async function toggleMod(modId, modsPath) {
|
||||
}
|
||||
}
|
||||
|
||||
async function syncModsForCurrentProfile() {
|
||||
try {
|
||||
const activeProfile = profileManager.getActiveProfile();
|
||||
if (!activeProfile) {
|
||||
console.warn('No active profile found during mod sync');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name}`);
|
||||
|
||||
const modsPath = await getModsPath();
|
||||
const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods');
|
||||
|
||||
if (!fs.existsSync(disabledModsPath)) {
|
||||
fs.mkdirSync(disabledModsPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Get all physical files from both folders
|
||||
const enabledFiles = fs.existsSync(modsPath) ? fs.readdirSync(modsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
||||
const disabledFiles = fs.existsSync(disabledModsPath) ? fs.readdirSync(disabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
||||
|
||||
const allFiles = new Set([...enabledFiles, ...disabledFiles]);
|
||||
|
||||
// Profile.mods contains the list of ALL mods for that profile, with their enabled state.
|
||||
|
||||
const profileMods = activeProfile.mods || [];
|
||||
|
||||
for (const fileName of allFiles) {
|
||||
const modConfig = profileMods.find(m => m.fileName === fileName);
|
||||
const shouldBeEnabled = modConfig && modConfig.enabled !== false; // Default to true if in list, unless explicitly false
|
||||
|
||||
// Logic:
|
||||
// If it should be enabled -> Move to mods/
|
||||
// If it should be disabled -> Move to DisabledMods/
|
||||
|
||||
const currentPath = enabledFiles.includes(fileName) ? path.join(modsPath, fileName) : path.join(disabledModsPath, fileName);
|
||||
const targetDir = shouldBeEnabled ? modsPath : disabledModsPath;
|
||||
const targetPath = path.join(targetDir, fileName);
|
||||
|
||||
if (path.dirname(currentPath) !== targetDir) {
|
||||
console.log(`[Mod Sync] Moving ${fileName} to ${shouldBeEnabled ? 'Enabled' : 'Disabled'}`);
|
||||
try {
|
||||
fs.renameSync(currentPath, targetPath);
|
||||
} catch (err) {
|
||||
console.error(`Failed to move ${fileName}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ModManager] Error syncing mods:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadInstalledMods,
|
||||
downloadMod,
|
||||
uninstallMod,
|
||||
toggleMod,
|
||||
syncModsForCurrentProfile,
|
||||
generateModId,
|
||||
extractModName,
|
||||
extractVersion
|
||||
|
||||
209
backend/managers/profileManager.js
Normal file
209
backend/managers/profileManager.js
Normal file
@@ -0,0 +1,209 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const {
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
getModsPath
|
||||
} = require('../core/config');
|
||||
|
||||
// We'll lazy-load modManager to avoid circular dependencies if possible,
|
||||
// or carefully structure our imports.
|
||||
// For now, we might need to access mod paths directly or use helper functions.
|
||||
|
||||
class ProfileManager {
|
||||
constructor() {
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.initialized) return;
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
// Migration: specific check to see if we have profiles yet
|
||||
if (!config.profiles || Object.keys(config.profiles).length === 0) {
|
||||
this.migrateLegacyConfig(config);
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
console.log('[ProfileManager] Initialized');
|
||||
}
|
||||
|
||||
migrateLegacyConfig(config) {
|
||||
console.log('[ProfileManager] Migrating legacy config to profile system...');
|
||||
|
||||
// Create a default profile with current settings
|
||||
const defaultProfileId = 'default';
|
||||
const defaultProfile = {
|
||||
id: defaultProfileId,
|
||||
name: 'Default',
|
||||
created: new Date().toISOString(),
|
||||
lastUsed: new Date().toISOString(),
|
||||
|
||||
// settings specific to this profile
|
||||
// If global settings existed, we copy them here
|
||||
mods: config.installedMods || [], // Legacy mods are now part of default profile
|
||||
javaPath: config.javaPath || '',
|
||||
gameOptions: {
|
||||
minMemory: '1G',
|
||||
maxMemory: '4G',
|
||||
args: []
|
||||
}
|
||||
};
|
||||
|
||||
const updates = {
|
||||
profiles: {
|
||||
[defaultProfileId]: defaultProfile
|
||||
},
|
||||
activeProfileId: defaultProfileId,
|
||||
// We keep a global registry of "known" mods if we want,
|
||||
// but for now the current implementation implies
|
||||
// mods are just files in folders.
|
||||
// We'll use the profile "mods" array to track ENABLED/KNOWN mods for that profile.
|
||||
};
|
||||
|
||||
saveConfig(updates);
|
||||
console.log('[ProfileManager] Migration complete. Created Default profile.');
|
||||
}
|
||||
|
||||
createProfile(name) {
|
||||
if (!name || typeof name !== 'string') {
|
||||
throw new Error('Invalid profile name');
|
||||
}
|
||||
|
||||
const config = loadConfig();
|
||||
const id = uuidv4();
|
||||
|
||||
const newProfile = {
|
||||
id,
|
||||
name: name.trim(),
|
||||
created: new Date().toISOString(),
|
||||
lastUsed: null,
|
||||
mods: [], // Start with no mods enabled
|
||||
javaPath: '',
|
||||
gameOptions: {
|
||||
minMemory: '1G',
|
||||
maxMemory: '4G',
|
||||
args: []
|
||||
}
|
||||
};
|
||||
|
||||
const profiles = config.profiles || {};
|
||||
profiles[id] = newProfile;
|
||||
|
||||
saveConfig({ profiles });
|
||||
|
||||
console.log(`[ProfileManager] Created new profile: "${name}" (${id})`);
|
||||
return newProfile;
|
||||
}
|
||||
|
||||
getProfiles() {
|
||||
const config = loadConfig();
|
||||
return Object.values(config.profiles || {});
|
||||
}
|
||||
|
||||
getProfile(id) {
|
||||
const config = loadConfig();
|
||||
return (config.profiles && config.profiles[id]) || null;
|
||||
}
|
||||
|
||||
getActiveProfile() {
|
||||
const config = loadConfig();
|
||||
const activeId = config.activeProfileId;
|
||||
if (!activeId || !config.profiles || !config.profiles[activeId]) {
|
||||
// Fallback if something is corrupted
|
||||
return this.getProfiles()[0] || null;
|
||||
}
|
||||
return config.profiles[activeId];
|
||||
}
|
||||
|
||||
async activateProfile(id) {
|
||||
const config = loadConfig();
|
||||
if (!config.profiles || !config.profiles[id]) {
|
||||
throw new Error(`Profile not found: ${id}`);
|
||||
}
|
||||
|
||||
if (config.activeProfileId === id) {
|
||||
console.log(`[ProfileManager] Profile ${id} is already active.`);
|
||||
return config.profiles[id];
|
||||
}
|
||||
|
||||
console.log(`[ProfileManager] Switching to profile: ${config.profiles[id].name} (${id})`);
|
||||
|
||||
// 1. Update config first
|
||||
config.profiles[id].lastUsed = new Date().toISOString();
|
||||
saveConfig({
|
||||
activeProfileId: id,
|
||||
profiles: config.profiles
|
||||
});
|
||||
|
||||
// 2. Trigger Mod Sync
|
||||
// We need to require this here to ensure it uses the *newly saved* active profile ID
|
||||
const { syncModsForCurrentProfile } = require('./modManager');
|
||||
await syncModsForCurrentProfile();
|
||||
|
||||
return config.profiles[id];
|
||||
}
|
||||
|
||||
deleteProfile(id) {
|
||||
const config = loadConfig();
|
||||
const profiles = config.profiles || {};
|
||||
|
||||
if (!profiles[id]) {
|
||||
throw new Error('Profile not found');
|
||||
}
|
||||
|
||||
if (config.activeProfileId === id) {
|
||||
throw new Error('Cannot delete the active profile');
|
||||
}
|
||||
|
||||
// Don't allow deleting the last profile
|
||||
if (Object.keys(profiles).length <= 1) {
|
||||
throw new Error('Cannot delete the only remaining profile');
|
||||
}
|
||||
|
||||
delete profiles[id];
|
||||
saveConfig({ profiles });
|
||||
console.log(`[ProfileManager] Deleted profile: ${id}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
updateProfile(id, updates) {
|
||||
const config = loadConfig();
|
||||
const profiles = config.profiles || {};
|
||||
|
||||
if (!profiles[id]) {
|
||||
throw new Error('Profile not found');
|
||||
}
|
||||
|
||||
// Safety checks on updates
|
||||
const allowedFields = ['name', 'javaPath', 'gameOptions', 'mods'];
|
||||
const sanitizedUpdates = {};
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
if (allowedFields.includes(key)) {
|
||||
sanitizedUpdates[key] = updates[key];
|
||||
}
|
||||
});
|
||||
|
||||
profiles[id] = { ...profiles[id], ...sanitizedUpdates };
|
||||
|
||||
saveConfig({ profiles });
|
||||
console.log(`[ProfileManager] Updated profile: ${id}`);
|
||||
|
||||
// If we updated mods for the *active* profile, we might need to sync immediately
|
||||
if (config.activeProfileId === id && updates.mods) {
|
||||
// Optionally trigger sync?
|
||||
// Usually updates come from "Enabling/Disabling" a single mod,
|
||||
// which might call a more specific method.
|
||||
// But if we bulk update, we should sync.
|
||||
// Let's leave sync invoke to the caller or specific methods for now.
|
||||
}
|
||||
|
||||
return profiles[id];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new ProfileManager();
|
||||
Reference in New Issue
Block a user