const express = require('express'); const fs = require('fs').promises; const path = require('path'); const paths = require('../utils/paths'); const ConfigParser = require('../utils/config-parser'); const router = express.Router(); // Mods listing page router.get('/', async (req, res) => { try { paths.ensureDirectories(); let globalMods = []; let worlds = []; // Get global mods try { const modDirs = await fs.readdir(paths.modsDir); for (const modDir of modDirs) { try { const modPath = paths.getModPath(modDir); const configPath = paths.getModConfigPath(modDir); const stats = await fs.stat(modPath); if (!stats.isDirectory()) continue; const config = await ConfigParser.parseModConfig(configPath); globalMods.push({ name: modDir, title: config.title || modDir, description: config.description || '', author: config.author || '', depends: config.depends || [], optional_depends: config.optional_depends || [], min_minetest_version: config.min_minetest_version || '', max_minetest_version: config.max_minetest_version || '', location: 'global', path: modPath, lastModified: stats.mtime }); } catch (modError) { console.error(`Error reading mod ${modDir}:`, modError); } } } catch (dirError) {} // Get worlds for dropdown try { const worldDirs = await fs.readdir(paths.worldsDir); for (const worldDir of worldDirs) { try { const worldPath = paths.getWorldPath(worldDir); const configPath = paths.getWorldConfigPath(worldDir); const stats = await fs.stat(worldPath); if (stats.isDirectory()) { const config = await ConfigParser.parseWorldConfig(configPath); worlds.push({ name: worldDir, displayName: config.server_name || worldDir }); } } catch {} } } catch {} const selectedWorld = req.query.world; let worldMods = []; if (selectedWorld && paths.isValidWorldName(selectedWorld)) { try { const worldModsPath = paths.getWorldModsPath(selectedWorld); const modDirs = await fs.readdir(worldModsPath); for (const modDir of modDirs) { try { const modPath = path.join(worldModsPath, modDir); const configPath = path.join(modPath, 'mod.conf'); const stats = await fs.stat(modPath); if (!stats.isDirectory()) continue; const config = await ConfigParser.parseModConfig(configPath); worldMods.push({ name: modDir, title: config.title || modDir, description: config.description || '', author: config.author || '', depends: config.depends || [], optional_depends: config.optional_depends || [], location: 'world', enabled: true, path: modPath, lastModified: stats.mtime }); } catch (modError) { console.error(`Error reading world mod ${modDir}:`, modError); } } } catch (dirError) {} } res.render('mods/index', { title: 'Mod Management', globalMods: globalMods, worldMods: worldMods, worlds: worlds, selectedWorld: selectedWorld, currentPage: 'mods' }); } catch (error) { console.error('Error getting mods:', error); res.status(500).render('error', { error: 'Failed to load mods', message: error.message }); } }); // Install mod to world router.post('/install/:worldName/:modName', async (req, res) => { try { const { worldName, modName } = req.params; if (!paths.isValidWorldName(worldName) || !paths.isValidModName(modName)) { return res.status(400).json({ error: 'Invalid world or mod name' }); } const worldPath = paths.getWorldPath(worldName); const globalModPath = paths.getModPath(modName); const worldModsPath = paths.getWorldModsPath(worldName); const targetModPath = path.join(worldModsPath, modName); try { await fs.access(worldPath); } catch { return res.status(404).json({ error: 'World not found' }); } try { await fs.access(globalModPath); } catch { return res.status(404).json({ error: 'Mod not found' }); } try { await fs.access(targetModPath); return res.status(409).json({ error: 'Mod already installed in world' }); } catch {} await fs.mkdir(worldModsPath, { recursive: true }); await fs.cp(globalModPath, targetModPath, { recursive: true }); res.redirect(`/mods?world=${worldName}&installed=${modName}`); } catch (error) { console.error('Error installing mod to world:', error); res.status(500).json({ error: 'Failed to install mod to world' }); } }); // Remove mod from world router.post('/remove/:worldName/:modName', async (req, res) => { try { const { worldName, modName } = req.params; if (!paths.isValidWorldName(worldName) || !paths.isValidModName(modName)) { return res.status(400).json({ error: 'Invalid world or mod name' }); } const worldModsPath = paths.getWorldModsPath(worldName); const modPath = path.join(worldModsPath, modName); try { await fs.access(modPath); } catch { return res.status(404).json({ error: 'Mod not found in world' }); } await fs.rm(modPath, { recursive: true, force: true }); res.redirect(`/mods?world=${worldName}&removed=${modName}`); } catch (error) { console.error('Error removing mod from world:', error); res.status(500).json({ error: 'Failed to remove mod from world' }); } }); // Delete global mod router.post('/delete/:modName', async (req, res) => { try { const { modName } = req.params; if (!paths.isValidModName(modName)) { return res.status(400).json({ error: 'Invalid mod name' }); } const modPath = paths.getModPath(modName); try { await fs.access(modPath); } catch { return res.status(404).json({ error: 'Mod not found' }); } await fs.rm(modPath, { recursive: true, force: true }); res.redirect(`/mods?deleted=${modName}`); } catch (error) { console.error('Error deleting mod:', error); res.status(500).json({ error: 'Failed to delete mod' }); } }); // Mod details page router.get('/:modName', async (req, res) => { try { const { modName } = req.params; if (!paths.isValidModName(modName)) { return res.status(400).render('error', { error: 'Invalid mod name' }); } const modPath = paths.getModPath(modName); const configPath = paths.getModConfigPath(modName); try { await fs.access(modPath); } catch { return res.status(404).render('error', { error: 'Mod not found' }); } const config = await ConfigParser.parseModConfig(configPath); const stats = await fs.stat(modPath); // Get mod files info let fileCount = 0; let totalSize = 0; async function countFiles(dirPath) { try { const items = await fs.readdir(dirPath); for (const item of items) { const itemPath = path.join(dirPath, item); const itemStats = await fs.stat(itemPath); if (itemStats.isDirectory()) { await countFiles(itemPath); } else { fileCount++; totalSize += itemStats.size; } } } catch {} } await countFiles(modPath); // Get worlds where this mod is installed const installedWorlds = []; try { const worldDirs = await fs.readdir(paths.worldsDir); for (const worldDir of worldDirs) { try { const worldModPath = path.join(paths.getWorldModsPath(worldDir), modName); await fs.access(worldModPath); const worldConfigPath = paths.getWorldConfigPath(worldDir); const worldConfig = await ConfigParser.parseWorldConfig(worldConfigPath); installedWorlds.push({ name: worldDir, displayName: worldConfig.server_name || worldDir }); } catch {} } } catch {} const modDetails = { name: modName, title: config.title || modName, description: config.description || '', author: config.author || '', depends: config.depends || [], optional_depends: config.optional_depends || [], min_minetest_version: config.min_minetest_version || '', max_minetest_version: config.max_minetest_version || '', location: 'global', path: modPath, fileCount, totalSize, created: stats.birthtime, lastModified: stats.mtime, installedWorlds: installedWorlds, config: config }; res.render('mods/details', { title: `Mod: ${modDetails.title}`, mod: modDetails, currentPage: 'mods' }); } catch (error) { console.error('Error getting mod details:', error); res.status(500).render('error', { error: 'Failed to load mod details', message: error.message }); } }); module.exports = router;