From a8da559e9310feda267dc16b37afec06a5be9294 Mon Sep 17 00:00:00 2001 From: Rahul Sahani <110347707+Rahul-Sahani04@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:31:57 +0530 Subject: [PATCH] Custom Mod loading fix (#92) * feat: Add Repair Game functionality including UserData backup and cache clearing * feat: Add In-App Logs Viewer and Logs Folder shortcut * feat: Add Open Logs feature * disable dev tools * Fix Settings UI * Implement custom mod loading, autoimport, auto repair * Fixed Custom Mod loading issues and merge issues * feat: Externalize sensitive API keys and Discord client ID into environment variables using dotenv. * feat(mods): add profile-based mod management and auto-repair --- .env.example | 2 + .gitignore | 4 +- GUI/index.html | 138 +++++++++------ GUI/js/mods.js | 15 +- backend/core/paths.js | 36 +++- backend/managers/modManager.js | 300 ++++++++++++++++++++++++--------- main.js | 9 +- package.json | 251 ++++++++++++++++----------- preload.js | 3 +- 9 files changed, 525 insertions(+), 233 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c1c8f4d --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +CURSEFORGE_API_KEY=$1234asdxXXXXXXkQCXXXXXXXXXXASDb32 +DISCORD_CLIENT_ID=561263XXXXXX \ No newline at end of file diff --git a/.gitignore b/.gitignore index 56f6219..e578779 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ pkg/ # Package files *.tar.zst -*.zst \ No newline at end of file +*.zst +bun.lockb +.env \ No newline at end of file diff --git a/GUI/index.html b/GUI/index.html index 9e1f4df..843aaad 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -118,22 +118,26 @@
- +
- +
- + @@ -159,7 +163,8 @@ READY TO PLAY -

Launch Hytale and enter the adventure

+

Launch Hytale and enter the + adventure

@@ -187,7 +193,8 @@
- +
- Page 1 of 1 + Page 1 of 1
@@ -316,8 +329,10 @@ onclick="repairGame()">
-
Repair Game
-
Reinstall game files (preserves data) +
Repair Game +
+
+ Reinstall game files (preserves data)
@@ -325,18 +340,25 @@
- +
- + - - - - + + + +

- Select your preferred GPU (Linux: affects DRI_PRIME) + Select your preferred GPU (Linux: + affects DRI_PRIME)

@@ -351,7 +373,8 @@
- +
@@ -365,7 +388,8 @@

- Your unique player identifier for this username + Your unique player identifier for + this username

@@ -375,8 +399,10 @@
@@ -394,8 +420,11 @@
-
Enable Discord Rich Presence
-
Show your launcher activity on Discord +
Enable + Discord Rich Presence
+
Show your launcher activity + on Discord
@@ -413,8 +442,10 @@
-
Use Custom Java Path
-
Override the bundled Java runtime with +
Use + Custom Java Path
+
+ Override the bundled Java runtime with your own installation
@@ -422,7 +453,8 @@
- +

@@ -447,7 +480,8 @@
- + @@ -470,15 +504,18 @@ Copy
-
Loading logs...
+
Loading + logs...

@@ -547,10 +584,12 @@ Choose a username to join the Players Chat

- + - 3-20 characters, letters, numbers, - and _ only + 3-20 characters, letters, numbers, - + and _ only
@@ -615,8 +654,7 @@

Set Custom UUID

+ data-i18n-placeholder="uuid.customPlaceholder" maxlength="36" />

- Warning: Setting a custom UUID will change your current player identity + Warning: Setting a custom UUID will change your current player + identity

@@ -648,8 +687,8 @@
- + @@ -735,12 +774,15 @@

Preview:

-
YourUsername
+
+ YourUsername
diff --git a/GUI/js/mods.js b/GUI/js/mods.js index 32f4ddd..631db3f 100644 --- a/GUI/js/mods.js +++ b/GUI/js/mods.js @@ -1,5 +1,5 @@ -const API_KEY = '$2a$10$bqk254NMZOWVTzLVJCcxEOmhcyUujKxA5xk.kQCN9q0KNYFJd5b32'; +let API_KEY = null; const CURSEFORGE_API = 'https://api.curseforge.com/v1'; const HYTALE_GAME_ID = 70216; @@ -11,6 +11,15 @@ let modsPageSize = 20; let modsTotalPages = 1; export async function initModsManager() { + try { + if (window.electronAPI && window.electronAPI.getEnvVar) { + API_KEY = await window.electronAPI.getEnvVar('CURSEFORGE_API_KEY'); + console.log('Loaded API Key:', API_KEY ? 'Yes' : 'No'); + } + } catch (err) { + console.error('Failed to load API Key:', err); + } + setupModsEventListeners(); await loadInstalledMods(); await loadBrowseMods(); @@ -417,10 +426,10 @@ async function deleteMod(modId) { const mod = installedMods.find(m => m.id === modId); if (!mod) return; - const confirmMsg = window.i18n ? + const confirmMsg = window.i18n ? window.i18n.t('mods.confirmDelete').replace('{name}', mod.name) + ' ' + window.i18n.t('mods.confirmDeleteDesc') : `Are you sure you want to delete "${mod.name}"? This action cannot be undone.`; - + showConfirmModal( confirmMsg, async () => { diff --git a/backend/core/paths.js b/backend/core/paths.js index e5ee8d0..b82de75 100644 --- a/backend/core/paths.js +++ b/backend/core/paths.js @@ -162,13 +162,18 @@ async function getModsPath(customInstallPath = null) { const modsPath = path.join(userDataPath, 'Mods'); const disabledModsPath = path.join(userDataPath, 'DisabledMods'); + const profilesPath = path.join(userDataPath, 'Profiles'); if (!fs.existsSync(modsPath)) { + // Ensure the Mods directory exists fs.mkdirSync(modsPath, { recursive: true }); } if (!fs.existsSync(disabledModsPath)) { fs.mkdirSync(disabledModsPath, { recursive: true }); } + if (!fs.existsSync(profilesPath)) { + fs.mkdirSync(profilesPath, { recursive: true }); + } return modsPath; } catch (error) { @@ -177,6 +182,34 @@ async function getModsPath(customInstallPath = null) { } } +function getProfilesDir(customInstallPath = null) { + try { + // get UserData path + let installPath = customInstallPath; + if (!installPath) { + const configFile = path.join(DEFAULT_APP_DIR, 'config.json'); + if (fs.existsSync(configFile)) { + const config = JSON.parse(fs.readFileSync(configFile, 'utf8')); + installPath = config.installPath || ''; + } + } + if (!installPath) installPath = getAppDir(); + + const gameLatest = path.join(installPath, 'release', 'package', 'game', 'latest'); + const userDataPath = findUserDataPath(gameLatest); + const profilesDir = path.join(userDataPath, 'Profiles'); + + if (!fs.existsSync(profilesDir)) { + fs.mkdirSync(profilesDir, { recursive: true }); + } + + return profilesDir; + } catch (err) { + console.error('Error getting profiles dir:', err); + return null; + } +} + module.exports = { getAppDir, getResolvedAppDir, @@ -191,5 +224,6 @@ module.exports = { findClientPath, findUserDataPath, findUserDataRecursive, - getModsPath + getModsPath, + getProfilesDir }; diff --git a/backend/managers/modManager.js b/backend/managers/modManager.js index 50c5744..5756e4e 100644 --- a/backend/managers/modManager.js +++ b/backend/managers/modManager.js @@ -2,10 +2,30 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const axios = require('axios'); -const { getModsPath } = require('../core/paths'); +const { getModsPath, getProfilesDir } = require('../core/paths'); const { saveModsToConfig, loadModsFromConfig } = require('../core/config'); const profileManager = require('./profileManager'); +const API_KEY = process.env.CURSEFORGE_API_KEY; + +/** + * 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); } @@ -35,30 +55,33 @@ function getProfileMods() { 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 || []; - 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'); + + // 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(physicalModsPath, modConfig.fileName)); - const inDisabled = fs.existsSync(path.join(disabledModsPath, modConfig.fileName)); + 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(physicalModsPath, modConfig.fileName) : path.join(disabledModsPath, modConfig.fileName), + filePath: inEnabled ? path.join(profileModsPath, modConfig.fileName) : path.join(profileDisabledModsPath, modConfig.fileName), enabled: modConfig.enabled !== false // Default true }); } else { @@ -82,7 +105,11 @@ async function loadInstalledMods(modsPath) { async function downloadMod(modInfo) { try { - const modsPath = await getModsPath(); + 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'); @@ -91,9 +118,9 @@ async function downloadMod(modInfo) { 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}`, { + 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, + 'x-api-key': modInfo.apiKey || API_KEY, 'Accept': 'application/json' } }); @@ -119,35 +146,30 @@ async function downloadMod(modInfo) { return new Promise((resolve, reject) => { writer.on('finish', () => { - // 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 - }; + // 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 }); + 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')); - } + resolve({ + success: true, + filePath: filePath, + fileName: fileName, + modInfo: newMod + }); }); writer.on('error', reject); }); @@ -173,8 +195,11 @@ async function uninstallMod(modId, modsPath) { throw new Error('Mod not found in profile'); } - const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods'); - const enabledPath = path.join(modsPath, mod.fileName); + // 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; @@ -226,31 +251,25 @@ async function toggleMod(modId, modsPath) { 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'); + // 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(modsPath, mod.fileName) : path.join(disabledModsPath, mod.fileName); - - // Determine target paths - - const targetDir = newEnabled ? modsPath : disabledModsPath; + 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)) { - // 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); + const altPath = mod.enabled ? path.join(disabledModsPath, mod.fileName) : path.join(profileModsPath, mod.fileName); if (fs.existsSync(altPath)) fs.renameSync(altPath, targetPath); } } @@ -273,35 +292,166 @@ async function syncModsForCurrentProfile() { return; } - console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name}`); + console.log(`[ModManager] Syncing mods for profile: ${activeProfile.name} (${activeProfile.id})`); - const modsPath = await getModsPath(); - const disabledModsPath = path.join(path.dirname(modsPath), 'DisabledMods'); + // 1. Resolve Paths + // globalModsPath is the one the game uses (symlink target) + const globalModsPath = await getModsPath(); + // profileModsPath is the real storage for this profile + const profileModsPath = getProfileModsPath(activeProfile.id); + const profileDisabledModsPath = path.join(path.dirname(profileModsPath), 'DisabledMods'); - if (!fs.existsSync(disabledModsPath)) { - fs.mkdirSync(disabledModsPath, { recursive: true }); + if (!fs.existsSync(profileDisabledModsPath)) { + fs.mkdirSync(profileDisabledModsPath, { 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')) : []; + // 2. Symlink / Migration Logic + let needsLink = false; + if (fs.existsSync(globalModsPath)) { + const stats = fs.lstatSync(globalModsPath); + + if (stats.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 (stats.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 { + fs.rmSync(globalModsPath, { recursive: true, force: true }); + 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}`); + try { + // 'junction' is key for Windows without admin + fs.symlinkSync(profileModsPath, globalModsPath, 'junction'); + } 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 }); + } + } + } + + // 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: Since Global/Mods IS Profile/Mods (via symlink), moving out of Profile/Mods disables it for the game. + + const disabledFiles = fs.existsSync(profileDisabledModsPath) ? fs.readdirSync(profileDisabledModsPath).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 + const shouldBeEnabled = modConfig && modConfig.enabled !== 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 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) { diff --git a/main.js b/main.js index ddd8091..e0ab2d0 100644 --- a/main.js +++ b/main.js @@ -1,5 +1,6 @@ -const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const path = require('path'); +require('dotenv').config({ path: path.join(__dirname, '.env') }); +const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const fs = require('fs'); const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); const UpdateManager = require('./backend/updateManager'); @@ -28,7 +29,7 @@ let updateManager; let discordRPC = null; // Discord Rich Presence setup -const DISCORD_CLIENT_ID = '1462244937868513373'; +const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID; function initDiscordRPC() { try { @@ -690,6 +691,10 @@ ipcMain.handle('get-local-app-data', async () => { return process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); }); +ipcMain.handle('get-env-var', async (event, key) => { + return process.env[key]; +}); + ipcMain.handle('get-user-id', async () => { try { const { getOrCreatePlayerId } = require('./backend/launcher'); diff --git a/package.json b/package.json index 51dd605..2abb38f 100644 --- a/package.json +++ b/package.json @@ -1,103 +1,150 @@ -{ - "name": "hytale-f2p-launcher", - "version": "2.0.2b", - "description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support", - "homepage": "https://github.com/amiayweb/Hytale-F2P", - "main": "main.js", - "scripts": { - "start": "electron .", - "dev": "electron . --dev", - "build": "electron-builder", - "build:win": "electron-builder --win", - "build:linux": "electron-builder --linux", - "build:mac": "electron-builder --mac", - "build:all": "electron-builder --win --linux --mac" - }, - "keywords": [ - "hytale", - "launcher", - "game", - "client", - "cross-platform", - "electron", - "auto-update", - "mod-manager", - "chat" - ], - "maintainers": [ - { - "name": "Terromur", - "url": "https://github.com/Terromur" - }, - { - "name": "Fari Gading", - "email": "fazrigading@gmail.com", - "url": "https://github.com/fazrigading" - } - ], - "author": { - "name": "AMIAY", - "email": "support@amiay.dev" - }, - "license": "MIT", - "devDependencies": { - "electron": "^40.0.0", - "electron-builder": "^26.4.0" - }, - "dependencies": { - "adm-zip": "^0.5.10", - "axios": "^1.6.0", - "discord-rpc": "^4.0.1", - "tar": "^6.2.1", - "uuid": "^9.0.1" - }, - "overrides": { - "tar": "$tar" - }, - "build": { - "appId": "com.hytalef2p.launcher", - "productName": "Hytale F2P Launcher", - "artifactName": "${name}_${version}_${arch}.${ext}", - "directories": { - "output": "dist" - }, - "files": [ - "main.js", - "preload.js", - "backend/**/*", - "GUI/**/*", - "package.json" - ], - "win": { - "target": [ - { "target": "nsis", "arch": ["x64", "arm64"] }, - { "target": "portable", "arch": ["x64"] } - ], - "icon": "icon.ico" - }, - "linux": { - "target": [ - { "target": "AppImage", "arch": ["x64", "arm64"] }, - { "target": "deb", "arch": ["x64", "arm64"] }, - { "target": "rpm", "arch": ["x64", "arm64"] }, - { "target": "pacman", "arch": ["x64", "arm64"] } - ], - "icon": "build/icon.png", - "category": "Game" - }, - "mac": { - "target": [ - { "target": "dmg", "arch": ["universal"] }, - { "target": "zip", "arch": ["universal"] } - ], - "icon": "build/icon.icns", - "category": "public.app-category.games" - }, - "nsis": { - "oneClick": false, - "allowToChangeInstallationDirectory": true, - "createDesktopShortcut": true, - "createStartMenuShortcut": true - } - } +{ + "name": "hytale-f2p-launcher", + "version": "2.0.2b", + "description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support", + "homepage": "https://github.com/amiayweb/Hytale-F2P", + "main": "main.js", + "scripts": { + "start": "electron .", + "dev": "electron . --dev", + "build": "electron-builder", + "build:win": "electron-builder --win", + "build:linux": "electron-builder --linux", + "build:mac": "electron-builder --mac", + "build:all": "electron-builder --win --linux --mac" + }, + "keywords": [ + "hytale", + "launcher", + "game", + "client", + "cross-platform", + "electron", + "auto-update", + "mod-manager", + "chat" + ], + "maintainers": [ + { + "name": "Terromur", + "url": "https://github.com/Terromur" + }, + { + "name": "Fari Gading", + "email": "fazrigading@gmail.com", + "url": "https://github.com/fazrigading" + } + ], + "author": { + "name": "AMIAY", + "email": "support@amiay.dev" + }, + "license": "MIT", + "devDependencies": { + "electron": "^40.0.0", + "electron-builder": "^26.4.0" + }, + "dependencies": { + "adm-zip": "^0.5.10", + "axios": "^1.6.0", + "discord-rpc": "^4.0.1", + "dotenv": "^17.2.3", + "tar": "^6.2.1", + "uuid": "^9.0.1" + }, + "overrides": { + "tar": "$tar" + }, + "build": { + "appId": "com.hytalef2p.launcher", + "productName": "Hytale F2P Launcher", + "artifactName": "${name}_${version}_${arch}.${ext}", + "directories": { + "output": "dist" + }, + "files": [ + "main.js", + "preload.js", + "backend/**/*", + "GUI/**/*", + "package.json", + ".env" + ], + "win": { + "target": [ + { + "target": "nsis", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "portable", + "arch": [ + "x64" + ] + } + ], + "icon": "icon.ico" + }, + "linux": { + "target": [ + { + "target": "AppImage", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "deb", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "rpm", + "arch": [ + "x64", + "arm64" + ] + }, + { + "target": "pacman", + "arch": [ + "x64", + "arm64" + ] + } + ], + "icon": "build/icon.png", + "category": "Game" + }, + "mac": { + "target": [ + { + "target": "dmg", + "arch": [ + "universal" + ] + }, + { + "target": "zip", + "arch": [ + "universal" + ] + } + ], + "icon": "build/icon.icns", + "category": "public.app-category.games" + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true + } + } } \ No newline at end of file diff --git a/preload.js b/preload.js index 84f4ed0..759eb5e 100644 --- a/preload.js +++ b/preload.js @@ -32,6 +32,7 @@ contextBridge.exposeInMainWorld('electronAPI', { openGameLocation: () => ipcRenderer.invoke('open-game-location'), saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings), loadSettings: () => ipcRenderer.invoke('load-settings'), + getEnvVar: (key) => ipcRenderer.invoke('get-env-var', key), getLocalAppData: () => ipcRenderer.invoke('get-local-app-data'), getModsPath: () => ipcRenderer.invoke('get-mods-path'), loadInstalledMods: (modsPath) => ipcRenderer.invoke('load-installed-mods', modsPath), @@ -59,7 +60,7 @@ contextBridge.exposeInMainWorld('electronAPI', { onUpdatePopup: (callback) => { ipcRenderer.on('show-update-popup', (event, data) => callback(data)); }, - + getGpuInfo: () => ipcRenderer.invoke('get-gpu-info'), saveGpuPreference: (gpuPreference) => ipcRenderer.invoke('save-gpu-preference', gpuPreference), loadGpuPreference: () => ipcRenderer.invoke('load-gpu-preference'),