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 @@
-
+
@@ -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'),