From 4775e9adbdd6f92c5ce8a0c6d0eda359128281e7 Mon Sep 17 00:00:00 2001 From: AMIAY Date: Thu, 29 Jan 2026 03:33:56 +0100 Subject: [PATCH] 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. --- GUI/index.html | 2 +- GUI/js/install.js | 21 +++++- GUI/js/settings.js | 7 ++ backend/managers/modManager.js | 130 +++++++++++---------------------- 4 files changed, 70 insertions(+), 90 deletions(-) diff --git a/GUI/index.html b/GUI/index.html index 8e8b4e1..7f3d624 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -120,7 +120,7 @@ + value="Player" maxlength="16" />
diff --git a/GUI/js/install.js b/GUI/js/install.js index a20d1ef..d7ea0d1 100644 --- a/GUI/js/install.js +++ b/GUI/js/install.js @@ -45,9 +45,17 @@ export function setupInstallation() { export async function installGame() { if (isDownloading || (installBtn && installBtn.disabled)) return; - const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player'; + let playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player'; const installPath = installPathInput ? installPathInput.value.trim() : ''; + // Limit player name to 16 characters + if (playerName.length > 16) { + playerName = playerName.substring(0, 16); + if (installPlayerName) { + installPlayerName.value = playerName; + } + } + const selectedBranchRadio = document.querySelector('input[name="installBranch"]:checked'); const selectedBranch = selectedBranchRadio ? selectedBranchRadio.value : 'release'; @@ -194,7 +202,16 @@ export async function browseInstallPath() { async function savePlayerName() { try { if (window.electronAPI && window.electronAPI.saveSettings) { - const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player'; + let playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player'; + + // Limit player name to 16 characters + if (playerName.length > 16) { + playerName = playerName.substring(0, 16); + if (installPlayerName) { + installPlayerName.value = playerName; + } + } + await window.electronAPI.saveSettings({ playerName }); } } catch (error) { diff --git a/GUI/js/settings.js b/GUI/js/settings.js index 0a2efc9..42b4c36 100644 --- a/GUI/js/settings.js +++ b/GUI/js/settings.js @@ -439,6 +439,13 @@ async function savePlayerName() { return; } + if (playerName.length > 16) { + const msg = window.i18n ? window.i18n.t('notifications.playerNameTooLong') : 'Player name must be 16 characters or less'; + showNotification(msg, 'error'); + settingsPlayerName.value = playerName.substring(0, 16); + return; + } + await window.electronAPI.saveUsername(playerName); const successMsg = window.i18n ? window.i18n.t('notifications.playerNameSaved') : 'Player name saved successfully'; showNotification(successMsg, 'success'); diff --git a/backend/managers/modManager.js b/backend/managers/modManager.js index 631db7f..cb7775e 100644 --- a/backend/managers/modManager.js +++ b/backend/managers/modManager.js @@ -3,7 +3,7 @@ const path = require('path'); const crypto = require('crypto'); const axios = require('axios'); const { getOS } = require('../utils/platformUtils'); -const { getModsPath, getProfilesDir } = require('../core/paths'); +const { getModsPath, getProfilesDir, getHytaleSavesDir } = require('../core/paths'); const { saveModsToConfig, loadModsFromConfig } = require('../core/config'); const profileManager = require('./profileManager'); @@ -296,8 +296,9 @@ async function syncModsForCurrentProfile() { console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name} (${activeProfile.id})`); // 1. Resolve Paths - // globalModsPath is the one the game uses (symlink target) - const globalModsPath = await getModsPath(); + // 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'); @@ -306,96 +307,51 @@ async function syncModsForCurrentProfile() { fs.mkdirSync(profileDisabledModsPath, { recursive: true }); } - // 2. Symlink / Migration Logic - let needsLink = false; - let globalStats = null; - + // 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 { - globalStats = fs.lstatSync(globalModsPath); + 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 + // Path doesn't exist, will be created above } - if (globalStats) { - if (globalStats.isSymbolicLink()) { - const linkTarget = fs.readlinkSync(globalModsPath); - // Normalize paths for comparison - if (path.resolve(linkTarget) !== path.resolve(profileModsPath)) { - console.log(`[ModManager] Updating symlink from ${linkTarget} to ${profileModsPath}`); - fs.unlinkSync(globalModsPath); - needsLink = true; - } - } else if (globalStats.isDirectory()) { - // MIGRATION: It's a real directory. Move contents to profile. - console.log('[ModManager] Migrating global mods folder to profile folder...'); - const files = fs.readdirSync(globalModsPath); - for (const file of files) { - const src = path.join(globalModsPath, file); - const dest = path.join(profileModsPath, file); - // Only move if dest doesn't exist to avoid overwriting - if (!fs.existsSync(dest)) { - fs.renameSync(src, dest); - } - } - - // Also migrate DisabledMods if it exists globally - const globalDisabledPath = path.join(path.dirname(globalModsPath), 'DisabledMods'); - if (fs.existsSync(globalDisabledPath) && fs.lstatSync(globalDisabledPath).isDirectory()) { - const dFiles = fs.readdirSync(globalDisabledPath); - for (const file of dFiles) { - const src = path.join(globalDisabledPath, file); - const dest = path.join(profileDisabledModsPath, file); - if (!fs.existsSync(dest)) { - fs.renameSync(src, dest); - } - } - // We can remove global DisabledMods now, as it's not used by game - try { fs.rmSync(globalDisabledPath, { recursive: true, force: true }); } catch(e) {} - } - - // Remove the directory so we can link it - try { - let retries = 3; - while (retries > 0) { - try { - fs.rmSync(globalModsPath, { recursive: true, force: true }); - break; - } catch (err) { - if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) { - retries--; - await new Promise(resolve => setTimeout(resolve, 500)); - } else { - throw err; - } - } - } - needsLink = true; - } catch (e) { - console.error('Failed to remove global mods dir:', e); - // Throw error to stop. - throw new Error('Failed to migrate mods directory. Please clear ' + globalModsPath); - } - } - } else { - needsLink = true; - } - - if (needsLink) { - console.log(`[ModManager] Creating symlink: ${globalModsPath} -> ${profileModsPath}`); + // 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 { - const symlinkType = getOS() === 'windows' ? 'junction' : 'dir'; - fs.symlinkSync(profileModsPath, globalModsPath, symlinkType); - } catch (err) { - // If we can't create the symlink, try creating the directory first - console.error('[ModManager] Failed to create symlink. Falling back to direct folder mode.'); - console.error(err.message); - - // Fallback: create a real directory so the game still works - if (!fs.existsSync(globalModsPath)) { - fs.mkdirSync(globalModsPath, { recursive: true }); + 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 || []; @@ -460,7 +416,7 @@ async function syncModsForCurrentProfile() { } // 5. Enforce Enabled/Disabled State (Move files between Profile/Mods and Profile/DisabledMods) - // Note: Since Global/Mods IS Profile/Mods (via symlink), moving out of Profile/Mods disables it for the game. + // 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]);