const { ModModel, ModAssetModel, ServerModPreferencesModel } = require('../models/ModModels'); const LocalDatabase = require('../config/LocalDatabase'); const logger = require('../utils/logger'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); class ModService { constructor() { this.modsDirectory = path.join(__dirname, '../../mods'); this.initialized = false; } async initialize() { try { // Initialize local database await LocalDatabase.initialize(); // Create mods directory if it doesn't exist if (!fs.existsSync(this.modsDirectory)) { fs.mkdirSync(this.modsDirectory, { recursive: true }); logger.info(`[MOD SERVICE] Created mods directory: ${this.modsDirectory}`); } // Load existing mods from filesystem await this.loadModsFromFilesystem(); this.initialized = true; logger.info('[MOD SERVICE] Mod service initialized successfully'); } catch (error) { logger.error('[MOD SERVICE] Failed to initialize mod service:', error); throw error; } } async loadModsFromFilesystem() { try { if (!fs.existsSync(this.modsDirectory)) { return; } const modFolders = fs.readdirSync(this.modsDirectory, { withFileTypes: true }) .filter(dirent => dirent.isDirectory()) .map(dirent => dirent.name); for (const modFolder of modFolders) { await this.loadModFromFolder(modFolder); } logger.info(`[MOD SERVICE] Loaded ${modFolders.length} mods from filesystem`); } catch (error) { logger.error('[MOD SERVICE] Error loading mods from filesystem:', error); } } async loadModFromFolder(modFolder) { try { const modPath = path.join(this.modsDirectory, modFolder); const manifestPath = path.join(modPath, 'mod.json'); if (!fs.existsSync(manifestPath)) { logger.warn(`[MOD SERVICE] Mod ${modFolder} missing mod.json manifest`); return; } const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); // Validate required fields if (!manifest.name || !manifest.version || !manifest.author) { logger.warn(`[MOD SERVICE] Mod ${modFolder} has invalid manifest`); return; } // Check if mod already exists in database const existingMod = await ModModel.findByName(manifest.name); // Calculate checksum of mod files const checksum = await this.calculateModChecksum(modPath); const modData = { name: manifest.name, version: manifest.version, author: manifest.author, description: manifest.description || '', filePath: modPath, checksum, dependencies: manifest.dependencies || [], config: manifest.config || {} }; if (existingMod) { // Update existing mod if checksum changed if (existingMod.checksum !== checksum) { await ModModel.update(existingMod.id, modData); logger.info(`[MOD SERVICE] Updated mod: ${manifest.name}`); } } else { // Create new mod await ModModel.create(modData); logger.info(`[MOD SERVICE] Installed new mod: ${manifest.name}`); } // Load mod assets await this.loadModAssets(manifest.name, modPath); } catch (error) { logger.error(`[MOD SERVICE] Error loading mod ${modFolder}:`, error); } } async loadModAssets(modName, modPath) { try { const mod = await ModModel.findByName(modName); if (!mod) return; // Clear existing assets for this mod await ModAssetModel.deleteByModId(mod.id); // Load assets from assets folder const assetsPath = path.join(modPath, 'assets'); if (fs.existsSync(assetsPath)) { await this.loadAssetsFromDirectory(mod.id, assetsPath); } logger.info(`[MOD SERVICE] Loaded assets for mod: ${modName}`); } catch (error) { logger.error(`[MOD SERVICE] Error loading assets for mod ${modName}:`, error); } } async loadAssetsFromDirectory(modId, assetsPath) { const assetTypes = ['ships', 'items', 'quests', 'systems']; for (const assetType of assetTypes) { const assetTypePath = path.join(assetsPath, assetType); if (fs.existsSync(assetTypePath)) { const assetFiles = fs.readdirSync(assetTypePath); for (const assetFile of assetFiles) { if (assetFile.endsWith('.json')) { try { const assetData = JSON.parse( fs.readFileSync(path.join(assetTypePath, assetFile), 'utf8') ); const assetId = path.basename(assetFile, '.json'); await ModAssetModel.create({ modId, assetType: assetType.slice(0, -1), // Remove 's' for singular assetId, assetData }); } catch (error) { logger.error(`[MOD SERVICE] Error loading asset ${assetFile}:`, error); } } } } } } async calculateModChecksum(modPath) { try { const hash = crypto.createHash('sha256'); // Hash all files in mod directory const hashDirectory = (dir) => { const files = fs.readdirSync(dir); files.forEach(file => { const filePath = path.join(dir, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { hashDirectory(filePath); } else { const data = fs.readFileSync(filePath); hash.update(data); } }); }; hashDirectory(modPath); return hash.digest('hex'); } catch (error) { logger.error(`[MOD SERVICE] Error calculating checksum for ${modPath}:`, error); return null; } } // Public API methods async getMods(enabledOnly = false) { return await ModModel.findAll(enabledOnly); } async getMod(id) { return await ModModel.findById(id); } async getModByName(name) { return await ModModel.findByName(name); } async enableMod(id) { const result = await ModModel.enable(id); if (result) { logger.info(`[MOD SERVICE] Enabled mod: ${id}`); } return result; } async disableMod(id) { const result = await ModModel.disable(id); if (result) { logger.info(`[MOD SERVICE] Disabled mod: ${id}`); } return result; } async getModAssets(assetType) { return await ModAssetModel.findByType(assetType); } async getModAsset(assetType, assetId) { return await ModAssetModel.findByTypeAndId(assetType, assetId); } async getLoadOrder() { return await ModModel.getLoadOrder(); } async setLoadOrder(modIds) { const result = await ModModel.setLoadOrder(modIds); if (result) { logger.info(`[MOD SERVICE] Updated mod load order`); } return result; } async getServerPreferences() { return await ServerModPreferencesModel.getAll(); } async setServerPreference(key, value) { return await ServerModPreferencesModel.set(key, value); } async reloadMods() { logger.info('[MOD SERVICE] Reloading mods from filesystem...'); await this.loadModsFromFilesystem(); } async shutdown() { if (this.initialized) { await LocalDatabase.close(); this.initialized = false; logger.info('[MOD SERVICE] Mod service shut down'); } } } module.exports = new ModService();