mirror of
https://github.com/amiayweb/Hytale-F2P.git
synced 2026-02-26 17:51:45 -03:00
* fix: resolve cross-platform EPERM permissions errors modManager.js: - Switch from hardcoded 'junction' to dynamic symlink type based on OS (fixing Linux EPERM). - Add retry logic for directory removal to handle file locking race conditions. - Improve broken symlink detection during profile sync. gameManager.js: - Implement retry loop (3 attempts) for game directory removal in updateGameFiles to prevent EBUSY/EPERM errors on Windows. paths.js: - Prevent fs.mkdirSync failure in getModsPath by pre-checking for broken symbolic links. * fix: missing pacman builds * prepare release for 2.1.1 minor fix for EPERM error permission * prepare release 2.1.1 minor fix EPERM permission error * prepare release 2.1.1 * Update README.md Windows Prequisites for ARM64 builds * fix: remove broken symlink after detected * fix: add pathexists for paths.js to check symlink * fix: isbrokenlink should be true to remove the symlink * add arch package .pkg.tar.zst for release * fix: release workflow for build-arch and build-linux * build-arch job now only build arch .pkg.tar.zst package instead of the whole generic linux. * build-linux job now exclude .pacman package since its deprecated and should not be used. * fix: removes pacman build as it replaced by tar.zst and adds build:arch shortcut for pkgbuild * aur: add proper VCS (-git) PKGBUILD created clean VCS-based PKGBUILD following arch packaging conventions. this explicitly marked as a rolling (-git) build and derives its version dynamically from git tags and commit history via pkgver(). previous hybrid approach has been changed. key changes: - use -git suffix to clearly indicate rolling source builds - set pkgver=0 and compute the actual version via pkgver() - build only a directory layout using electron-builder (--dir) - avoid generating AppImage, deb, rpm, or pacman installers - align build and package steps with Arch packaging guidelines note: this PKGBUILD is intended for development and AUR use only and is not suitable for binary redistribution or release artifacts. * ci: add fixed-version PKGBUILD for Arch Linux releases this PKGBUILD intended for CI and GitHub release artifacts. targets tagged releases only and uses a fixed pkgver that matches the corresponding git tag. all of the VCS logic has been removed to PKGBUILD-git to ensure reproducible builds and stable versioning suitable for binary distribution. the build process relies on electron-builder directory output (--dir) and packages only the unpacked application into a standard Arch Linux package (.pkg.tar.zst). other distro format are excluded from this path and handled separately. this change establishes a clear separation between: - rolling AUR development builds (-git) - CI-generated, versioned Arch Linux release packages the result is predictable artifact naming, correct version alignment, and Arch-compliant packaging for downstream users. * Update README.md adds information for Arch build * Update README.md BUILD.md location was changed and now this link is poiting to nothing * Update PKGBUILD * Update PKGBUILD-git * chore: fix ubuntu/debian part in README.md * Polish language support (#195) * Update support_request.yml Added hardware specification * Update bug_report.yml Add logs textfield to bug report * chore: add changelog in README.md * fix screenshot input in feature_request.yml * add hardware spec input in bug_report.yml * fix: PKGBUILD pkgname variable fix * userdata migration [need review from other OS] * french translate * Add German and Swedish translations Added de.json and sv.json locale files for German and Swedish language support. Updated i18n.js to register 'de' and 'sv' as available languages in the launcher. * Update README.md * chore: add offline-mode warning to the README.md * chore: add downloads counter in README.md * fix: Steam Deck/Ubuntu crash - use system libzstd.so The bundled libzstd.so is incompatible with glibc 2.41's stricter heap validation, causing "free(): invalid pointer" crashes. Solution: Automatically replace bundled libzstd.so with system version on Linux. The launcher detects and symlinks to /usr/lib/libzstd.so.1. - Auto-detect system libzstd at common paths (Arch, Debian, Fedora) - Backup bundled version as libzstd.so.bundled - Create symlink to system version - Add HYTALE_NO_LIBZSTD_FIX=1 to disable if needed Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: remove Windows and Linux ARM64 information on the README.md * Update support_request.yml * fix: improve update system UX and macOS compatibility Update System Improvements: - Fix duplicate update popups by disabling legacy updater.js - Add skip button to update popup (shows after 30s, on error, or after download) - Add macOS-specific handling with manual download as primary option - Add missing open-download-page IPC handler - Add missing unblockInterface() method to properly clean up after popup close - Add quitAndInstallUpdate alias in preload for compatibility - Remove pulse animation when download completes - Fix manual download button to show correct status and close popup - Sync player name to settings input after first install Client Patcher Cleanup: - Remove server patching code (server uses pre-patched JAR from CDN) - Simplify to client-only patching - Remove unused imports (crypto, AdmZip, execSync, spawn, javaManager) - Remove unused methods (stringToUtf8, findAndReplaceDomainUtf8) - Move localhost dev code to backup file for reference Code Quality Fixes: - Fix duplicate DOMContentLoaded handlers in install.js - Fix duplicate checkForUpdates definition in preload.js - Fix redundant if/else in onProgressUpdate callback - Fix typo "Harwadre" -> "Hardware" in preload.js Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add Russian language support Added Russian (ru) to the list of available languages. * chore: drafting documentation on SERVER.md * Some updates in Russian language localization file * fix * Update ru.json * Fixed Java runtime name and fixed typo * fixed untranslated place * Update ru.json * Update ru.json * Update ru.json * Update ru.json * Update ru.json * fix: timeout getLatestClient fixes #138 * fix: change default version to 7.pwr in main.js * fix: change default release version to 7.pwr * fix: change version release to 7.pwr * docs: Add comprehensive troubleshooting guide (#209) Add TROUBLESHOOTING.md with solutions for common issues including: - Windows: Firewall configuration, duplicate mods, SmartScreen - Linux: GPU detection (NVIDIA/AMD), SDL3_image/libpng dependencies, Wayland/X11 issues, Steam Deck support - macOS: Rosetta 2 for Apple Silicon, code signing, quarantine - Connection: Server boot failures, regional restrictions - Authentication: Token errors, config reset procedures - Avatar/Cosmetics: F2P limitations documentation - Backup locations for all platforms - Log locations for bug reports Solutions compiled from closed GitHub issues (#205, #155, #90, #60, #144, #192) and community feedback. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * Standardize language codes, improve formatting, and update all locale files. (#224) * Update German (Germany) localization * Update Español (España) localization * Update French (France) localization * Update Polish (Poland) localization * Update Portuguese (Brazil) localization * Update Russian (Russia) localization * Update Swedish (Sweden) localization * Update Turkish (Turkey) localization * Update language codes, names and alphabetical in i18n system * Changed Spanish language name to the Formal name "Spanish (Spain)" * Fix PKGBUILD-git * Fix PKGBUILD * delete cache after installation * Enforce 16-char player name limit and update mod sync Added a maxlength attribute to the player name input and enforced a 16-character limit in both install and settings scripts, providing user feedback if exceeded. Refactored modManager.js to replace symlink-based mod management with a copy-based system, copying enabled mods to HytaleSaves\Mods and removing legacy symlink logic to improve compatibility and avoid permission issues. * Update installation subtitle * chore: update quickstart link in README.md * chore: delete warning of Ubuntu-Debian at Linux Prequisites section * added featured server list from api * Add Featured Servers page to GUI * Update Discord invite URL in client patcher * Add differential update system * Remove launcher chat and add Discord popup * fix: removed 'check disk space' alert on permission file error * fix: upgrade tar to ^7.5.6 version * fix: re-add universal arch for mac * fix: upgrade electron/rebuild to 4.0.3 * fix: removed override tar version * fix: pkgbuild version to 2.1.2 * fix: src.tar.zst and srcinfo missing files * feat: add Indonesian language translation * fix: GPU preference hint to Laptop-only * feat: create two columns for settings page * Add Discord invite link to rpc * docs: add recordings form, fix OS list * Release v2.2.0 * Release v2.2.0 * Release v2.2.0 * chore: delete icon.ico, moved to build folder * chore: delete icon.png, moved to build folder * fix: build and release for tag push-only in release.yml * fix: gamescope steam deck issue fixes #186 hopefully * Support branch selection for server patching * chose: add auto-patch system for pre-release JAR --------- Co-authored-by: TalesAmaral <57869141+TalesAmaral@users.noreply.github.com> Co-authored-by: walti0 <95646872+walti0@users.noreply.github.com> Co-authored-by: AMIAY <letudiantenrap.collab@gmail.com> Co-authored-by: sanasol <mail@sanasol.ws> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Terromur <79866197+Terromur@users.noreply.github.com> Co-authored-by: Zakhar Smokotov <zaharb840@gmail.com> Co-authored-by: xSamiVS <samtaiebc@gmail.com>
460 lines
16 KiB
JavaScript
460 lines
16 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const axios = require('axios');
|
|
const { getOS } = require('../utils/platformUtils');
|
|
const { getModsPath, getProfilesDir, getHytaleSavesDir } = require('../core/paths');
|
|
const { saveModsToConfig, loadModsFromConfig } = require('../core/config');
|
|
const profileManager = require('./profileManager');
|
|
|
|
const API_KEY = "$2a$10$bqk254NMZOWVTzLVJCcxEOmhcyUujKxA5xk.kQCN9q0KNYFJd5b32";
|
|
|
|
/**
|
|
* Get the physical mods path for a specific profile.
|
|
* Each profile now has its own 'mods' folder.
|
|
*/
|
|
function getProfileModsPath(profileId) {
|
|
const profilesDir = getProfilesDir();
|
|
if (!profilesDir) return null;
|
|
|
|
const profileDir = path.join(profilesDir, profileId);
|
|
const modsDir = path.join(profileDir, 'mods');
|
|
|
|
if (!fs.existsSync(modsDir)) {
|
|
fs.mkdirSync(modsDir, { recursive: true });
|
|
}
|
|
|
|
return modsDir;
|
|
}
|
|
|
|
function generateModId(filename) {
|
|
return crypto.createHash('md5').update(filename).digest('hex').substring(0, 8);
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
function extractVersion(filename) {
|
|
const versionMatch = filename.match(/v?(\d+\.[\d\.]+)/);
|
|
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 {
|
|
// Sync first to ensure we detect any manually added mods and paths are correct
|
|
await syncModsForCurrentProfile();
|
|
|
|
const activeProfile = profileManager.getActiveProfile();
|
|
if (!activeProfile) return [];
|
|
|
|
const profileMods = activeProfile.mods || [];
|
|
|
|
// Use profile-specific paths
|
|
const profileModsPath = getProfileModsPath(activeProfile.id);
|
|
const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
|
|
|
|
if (!fs.existsSync(profileModsPath)) fs.mkdirSync(profileModsPath, { recursive: true });
|
|
if (!fs.existsSync(profileDisabledModsPath)) fs.mkdirSync(profileDisabledModsPath, { recursive: true });
|
|
|
|
const validMods = [];
|
|
|
|
for (const modConfig of profileMods) {
|
|
// Check if file exists in either location
|
|
const inEnabled = fs.existsSync(path.join(profileModsPath, modConfig.fileName));
|
|
const inDisabled = fs.existsSync(path.join(profileDisabledModsPath, modConfig.fileName));
|
|
|
|
if (inEnabled || inDisabled) {
|
|
validMods.push({
|
|
...modConfig,
|
|
// Set filePath based on physical location
|
|
filePath: inEnabled ? path.join(profileModsPath, modConfig.fileName) : path.join(profileDisabledModsPath, 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 validMods;
|
|
} catch (error) {
|
|
console.error('Error loading installed mods:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function downloadMod(modInfo) {
|
|
try {
|
|
const activeProfile = profileManager.getActiveProfile();
|
|
if (!activeProfile) throw new Error('No active profile to save mod to');
|
|
|
|
const modsPath = getProfileModsPath(activeProfile.id);
|
|
if (!modsPath) throw new Error('Could not determine profile mods path');
|
|
|
|
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 || modInfo.curseForgeId}/files/${modInfo.fileId || modInfo.curseForgeFileId}`, {
|
|
headers: {
|
|
'x-api-key': modInfo.apiKey || API_KEY,
|
|
'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', () => {
|
|
// Update Active Profile
|
|
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
|
|
});
|
|
});
|
|
writer.on('error', reject);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('Error downloading mod:', error);
|
|
return {
|
|
success: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
async function uninstallMod(modId, modsPath) {
|
|
try {
|
|
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 profile');
|
|
}
|
|
|
|
// Use profile paths
|
|
const profileModsPath = getProfileModsPath(activeProfile.id);
|
|
const disabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
|
|
|
|
const enabledPath = path.join(profileModsPath, 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;
|
|
}
|
|
if (fs.existsSync(disabledPath)) {
|
|
try { fs.unlinkSync(disabledPath); fileRemoved = true; } catch (e) { }
|
|
}
|
|
|
|
if (!fileRemoved) {
|
|
console.warn('Mod file not found on filesystem, removing from profile anyway');
|
|
}
|
|
|
|
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);
|
|
return {
|
|
success: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
async function toggleMod(modId, modsPath) {
|
|
try {
|
|
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 });
|
|
|
|
// Move file between Profile/Mods and Profile/DisabledMods
|
|
const profileModsPath = getProfileModsPath(activeProfile.id);
|
|
const disabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
|
|
|
|
if (!fs.existsSync(disabledModsPath)) fs.mkdirSync(disabledModsPath, { recursive: true });
|
|
|
|
const currentPath = mod.enabled ? path.join(profileModsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName);
|
|
const targetDir = newEnabled ? profileModsPath : disabledModsPath;
|
|
const targetPath = path.join(targetDir, mod.fileName);
|
|
|
|
if (fs.existsSync(currentPath)) {
|
|
fs.renameSync(currentPath, targetPath);
|
|
} else {
|
|
// Fallback: check if it's already in target?
|
|
if (fs.existsSync(targetPath)) {
|
|
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(profileModsPath, mod.fileName);
|
|
if (fs.existsSync(altPath)) fs.renameSync(altPath, targetPath);
|
|
}
|
|
}
|
|
|
|
return { success: true, enabled: newEnabled };
|
|
} catch (error) {
|
|
console.error('Error toggling mod:', error);
|
|
return {
|
|
success: false,
|
|
error: error.message
|
|
};
|
|
}
|
|
}
|
|
|
|
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} (${activeProfile.id})`);
|
|
|
|
// 1. Resolve Paths
|
|
// centralModsPath is HytaleSaves\Mods (centralized location for active mods)
|
|
const hytaleSavesDir = getHytaleSavesDir();
|
|
const centralModsPath = path.join(hytaleSavesDir, 'Mods');
|
|
// profileModsPath is the real storage for this profile
|
|
const profileModsPath = getProfileModsPath(activeProfile.id);
|
|
const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods');
|
|
|
|
if (!fs.existsSync(profileDisabledModsPath)) {
|
|
fs.mkdirSync(profileDisabledModsPath, { recursive: true });
|
|
}
|
|
|
|
// 2. Copy-based Mod Sync (No symlinks - avoids permission issues)
|
|
// Ensure HytaleSaves\Mods directory exists
|
|
if (!fs.existsSync(centralModsPath)) {
|
|
fs.mkdirSync(centralModsPath, { recursive: true });
|
|
console.log(`[ModManager] Created centralized mods directory: ${centralModsPath}`);
|
|
}
|
|
|
|
// Check for old symlink and convert to real directory if needed (one-time migration)
|
|
try {
|
|
const centralStats = fs.lstatSync(centralModsPath);
|
|
if (centralStats.isSymbolicLink()) {
|
|
console.log('[ModManager] Removing old symlink, converting to copy-based system...');
|
|
fs.unlinkSync(centralModsPath);
|
|
fs.mkdirSync(centralModsPath, { recursive: true });
|
|
}
|
|
} catch (e) {
|
|
// Path doesn't exist, will be created above
|
|
}
|
|
|
|
// Copy enabled mods from profile to HytaleSaves\Mods (for game to use)
|
|
console.log(`[ModManager] Copying enabled mods from ${profileModsPath} to ${centralModsPath}`);
|
|
|
|
// First, clear central mods folder
|
|
const existingCentralMods = fs.existsSync(centralModsPath) ? fs.readdirSync(centralModsPath) : [];
|
|
for (const file of existingCentralMods) {
|
|
const filePath = path.join(centralModsPath, file);
|
|
try {
|
|
fs.unlinkSync(filePath);
|
|
} catch (e) {
|
|
console.warn(`Failed to remove ${file} from central mods:`, e.message);
|
|
}
|
|
}
|
|
|
|
// Copy enabled mods to HytaleSaves\Mods
|
|
const enabledModFiles = fs.existsSync(profileModsPath) ? fs.readdirSync(profileModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
|
for (const file of enabledModFiles) {
|
|
const src = path.join(profileModsPath, file);
|
|
const dest = path.join(centralModsPath, file);
|
|
try {
|
|
fs.copyFileSync(src, dest);
|
|
console.log(`[ModManager] Copied ${file} to HytaleSaves\\Mods`);
|
|
} catch (e) {
|
|
console.error(`Failed to copy ${file}:`, e.message);
|
|
}
|
|
}
|
|
|
|
// 3. Auto-Repair (Download missing mods)
|
|
const profileModsSnapshot = activeProfile.mods || [];
|
|
for (const mod of profileModsSnapshot) {
|
|
if (mod.enabled && !mod.manual) {
|
|
const inEnabled = fs.existsSync(path.join(profileModsPath, mod.fileName));
|
|
const inDisabled = fs.existsSync(path.join(profileDisabledModsPath, mod.fileName));
|
|
|
|
if (!inEnabled && !inDisabled) {
|
|
if (mod.curseForgeId && (mod.curseForgeFileId || mod.fileId)) {
|
|
console.log(`[ModManager] Auto-repair: Re-downloading missing mod "${mod.name}"...`);
|
|
try {
|
|
await downloadMod({
|
|
...mod,
|
|
modId: mod.curseForgeId,
|
|
fileId: mod.curseForgeFileId || mod.fileId,
|
|
apiKey: API_KEY
|
|
});
|
|
} catch (err) {
|
|
console.error(`[ModManager] Auto-repair failed for "${mod.name}": ${err.message}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Auto-Import (Detect manual drops in the profile folder)
|
|
const enabledFiles = fs.existsSync(profileModsPath) ? fs.readdirSync(profileModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
|
|
|
let profileMods = activeProfile.mods || [];
|
|
let profileUpdated = false;
|
|
|
|
|
|
// Anything in this folder belongs to this profile.
|
|
|
|
for (const file of enabledFiles) {
|
|
const isKnown = profileMods.some(m => m.fileName === file);
|
|
|
|
if (!isKnown) {
|
|
console.log(`[ModManager] Auto-importing manual mod: ${file}`);
|
|
const newMod = {
|
|
id: generateModId(file),
|
|
name: extractModName(file),
|
|
version: 'Unknown',
|
|
description: 'Manually installed',
|
|
author: 'Local',
|
|
enabled: true,
|
|
fileName: file,
|
|
fileSize: 0,
|
|
dateInstalled: new Date().toISOString(),
|
|
manual: true
|
|
};
|
|
profileMods.push(newMod);
|
|
profileUpdated = true;
|
|
}
|
|
}
|
|
|
|
if (profileUpdated) {
|
|
profileManager.updateProfile(activeProfile.id, { mods: profileMods });
|
|
const updatedProfile = profileManager.getActiveProfile();
|
|
profileMods = updatedProfile ? (updatedProfile.mods || []) : profileMods;
|
|
}
|
|
|
|
// 5. Enforce Enabled/Disabled State (Move files between Profile/Mods and Profile/DisabledMods)
|
|
// Note: Enabled mods are copied to HytaleSaves\Mods, disabled mods stay in Profile/DisabledMods
|
|
|
|
const disabledFiles = fs.existsSync(profileDisabledModsPath) ? fs.readdirSync(profileDisabledModsPath).filter(f => f.endsWith('.jar') || f.endsWith('.zip')) : [];
|
|
const allFiles = new Set([...enabledFiles, ...disabledFiles]);
|
|
|
|
for (const fileName of allFiles) {
|
|
const modConfig = profileMods.find(m => m.fileName === fileName);
|
|
const shouldBeEnabled = modConfig && modConfig.enabled !== false;
|
|
|
|
const currentPath = enabledFiles.includes(fileName) ? path.join(profileModsPath, fileName) : path.join(profileDisabledModsPath, fileName);
|
|
const targetDir = shouldBeEnabled ? profileModsPath : profileDisabledModsPath;
|
|
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
|
|
};
|