265 lines
7.4 KiB
JavaScript
265 lines
7.4 KiB
JavaScript
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();
|