API/GameServer/services/ModService.js
2026-01-24 16:47:19 -04:00

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();