const express = require('express'); const fs = require('fs').promises; const path = require('path'); const sqlite3 = require('sqlite3'); const { promisify } = require('util'); const paths = require('../utils/paths'); const ConfigParser = require('../utils/config-parser'); const router = express.Router(); // Worlds listing page router.get('/', async (req, res) => { try { paths.ensureDirectories(); let worlds = []; try { const worldDirs = await fs.readdir(paths.worldsDir); for (const worldDir of worldDirs) { const worldPath = paths.getWorldPath(worldDir); const configPath = paths.getWorldConfigPath(worldDir); try { const stats = await fs.stat(worldPath); if (!stats.isDirectory()) continue; const config = await ConfigParser.parseWorldConfig(configPath); let playerCount = 0; try { const playersDbPath = path.join(worldPath, 'players.sqlite'); const db = new sqlite3.Database(playersDbPath); const all = promisify(db.all.bind(db)); const result = await all('SELECT COUNT(*) as count FROM players'); playerCount = result[0]?.count || 0; db.close(); } catch (dbError) {} worlds.push({ name: worldDir, displayName: config.server_name || worldDir, description: config.server_description || '', gameid: config.gameid || 'minetest_game', creativeMode: config.creative_mode || false, enableDamage: config.enable_damage !== false, enablePvp: config.enable_pvp !== false, playerCount, lastModified: stats.mtime, size: stats.size }); } catch (worldError) { console.error(`Error reading world ${worldDir}:`, worldError); } } } catch (dirError) {} res.render('worlds/index', { title: 'Worlds', worlds: worlds, currentPage: 'worlds' }); } catch (error) { console.error('Error getting worlds:', error); res.status(500).render('error', { error: 'Failed to load worlds', message: error.message }); } }); // New world page router.get('/new', async (req, res) => { try { const games = await paths.getInstalledGames(); res.render('worlds/new', { title: 'Create World', currentPage: 'worlds', games: games }); } catch (error) { console.error('Error getting games for new world:', error); res.render('worlds/new', { title: 'Create World', currentPage: 'worlds', games: [ { name: 'minetest_game', title: 'Minetest Game (Default)', description: '' }, { name: 'minimal', title: 'Minimal', description: '' } ], error: 'Could not load installed games, showing defaults only.' }); } }); // Create world router.post('/create', async (req, res) => { console.log('=== WORLD CREATION STARTED ==='); console.log('Request body:', req.body); try { const { name, gameid } = req.body; console.log('Extracted name:', name, 'gameid:', gameid); if (!paths.isValidWorldName(name)) { return res.status(400).render('worlds/new', { title: 'Create World', currentPage: 'worlds', error: 'Invalid world name. Only letters, numbers, underscore and hyphen allowed.', formData: req.body }); } const worldPath = paths.getWorldPath(name); try { await fs.access(worldPath); return res.status(409).render('worlds/new', { title: 'Create World', currentPage: 'worlds', error: 'World already exists', formData: req.body }); } catch {} console.log('Starting world creation for:', name, 'with gameid:', gameid); // Create the world directory - Luanti will initialize it when the server starts await fs.mkdir(worldPath, { recursive: true }); console.log('Created world directory:', worldPath); // Create a basic world.mt file with the correct game ID const worldConfig = `enable_damage = true creative_mode = false mod_storage_backend = sqlite3 auth_backend = sqlite3 player_backend = sqlite3 backend = sqlite3 gameid = ${gameid || 'minetest_game'} world_name = ${name} `; const worldConfigPath = path.join(worldPath, 'world.mt'); await fs.writeFile(worldConfigPath, worldConfig, 'utf8'); console.log('Created world.mt with gameid:', gameid || 'minetest_game'); // Create essential database files with proper schema const sqlite3 = require('sqlite3'); // Create players database with correct schema const playersDbPath = path.join(worldPath, 'players.sqlite'); await new Promise((resolve, reject) => { const playersDb = new sqlite3.Database(playersDbPath, (err) => { if (err) reject(err); else { playersDb.serialize(() => { playersDb.exec(`CREATE TABLE IF NOT EXISTS player ( name TEXT PRIMARY KEY, pitch REAL, yaw REAL, posX REAL, posY REAL, posZ REAL, hp INTEGER, breath INTEGER, creation_date INTEGER, modification_date INTEGER, privs TEXT )`, (err) => { if (err) { console.error('Error creating player table:', err); reject(err); } else { console.log('Created player table in players.sqlite'); playersDb.close((closeErr) => { if (closeErr) reject(closeErr); else resolve(); }); } }); }); } }); }); // Create other essential databases const mapDbPath = path.join(worldPath, 'map.sqlite'); await new Promise((resolve, reject) => { const mapDb = new sqlite3.Database(mapDbPath, (err) => { if (err) reject(err); else { mapDb.serialize(() => { mapDb.exec(`CREATE TABLE IF NOT EXISTS blocks ( x INTEGER, y INTEGER, z INTEGER, data BLOB NOT NULL, PRIMARY KEY (x, z, y) )`, (err) => { if (err) { console.error('Error creating blocks table:', err); reject(err); } else { console.log('Created blocks table in map.sqlite'); mapDb.close((closeErr) => { if (closeErr) reject(closeErr); else resolve(); }); } }); }); } }); }); const modStorageDbPath = path.join(worldPath, 'mod_storage.sqlite'); await new Promise((resolve, reject) => { const modDb = new sqlite3.Database(modStorageDbPath, (err) => { if (err) reject(err); else { modDb.serialize(() => { modDb.exec(`CREATE TABLE IF NOT EXISTS entries ( modname TEXT NOT NULL, key BLOB NOT NULL, value BLOB NOT NULL, PRIMARY KEY (modname, key) )`, (err) => { if (err) { console.error('Error creating entries table:', err); reject(err); } else { console.log('Created entries table in mod_storage.sqlite'); modDb.close((closeErr) => { if (closeErr) reject(closeErr); else resolve(); }); } }); }); } }); }); console.log('Created essential database files with proper schema'); res.redirect('/worlds?created=' + encodeURIComponent(name)); } catch (error) { console.error('Error creating world:', error); res.status(500).render('worlds/new', { title: 'Create World', currentPage: 'worlds', error: 'Failed to create world: ' + error.message, formData: req.body }); } }); // World details page router.get('/:worldName', async (req, res) => { try { const { worldName } = req.params; if (!paths.isValidWorldName(worldName)) { return res.status(400).render('error', { error: 'Invalid world name' }); } const worldPath = paths.getWorldPath(worldName); const configPath = paths.getWorldConfigPath(worldName); try { await fs.access(worldPath); } catch { return res.status(404).render('error', { error: 'World not found' }); } const config = await ConfigParser.parseWorldConfig(configPath); const stats = await fs.stat(worldPath); let worldSize = 0; try { const mapDbPath = path.join(worldPath, 'map.sqlite'); const mapStats = await fs.stat(mapDbPath); worldSize = mapStats.size; } catch {} let enabledMods = []; try { const worldModsPath = paths.getWorldModsPath(worldName); const modDirs = await fs.readdir(worldModsPath); for (const modDir of modDirs) { const modConfigPath = path.join(worldModsPath, modDir, 'mod.conf'); try { const modConfig = await ConfigParser.parseModConfig(modConfigPath); enabledMods.push({ name: modDir, title: modConfig.title || modDir, description: modConfig.description || '', author: modConfig.author || '', location: 'world' }); } catch {} } } catch {} const worldDetails = { name: worldName, displayName: config.server_name || worldName, description: config.server_description || '', gameid: config.gameid || 'minetest_game', creativeMode: config.creative_mode || false, enableDamage: config.enable_damage !== false, enablePvp: config.enable_pvp !== false, serverAnnounce: config.server_announce || false, worldSize, created: stats.birthtime, lastModified: stats.mtime, enabledMods, config: config }; res.render('worlds/details', { title: `World: ${worldDetails.displayName}`, world: worldDetails, currentPage: 'worlds' }); } catch (error) { console.error('Error getting world details:', error); res.status(500).render('error', { error: 'Failed to load world details', message: error.message }); } }); // Update world router.post('/:worldName/update', async (req, res) => { try { const { worldName } = req.params; const updates = req.body; if (!paths.isValidWorldName(worldName)) { return res.status(400).json({ error: 'Invalid world name' }); } const worldPath = paths.getWorldPath(worldName); const configPath = paths.getWorldConfigPath(worldName); try { await fs.access(worldPath); } catch { return res.status(404).json({ error: 'World not found' }); } const currentConfig = await ConfigParser.parseWorldConfig(configPath); // Convert form data const updatedConfig = { ...currentConfig, server_name: updates.displayName || currentConfig.server_name, server_description: updates.description || currentConfig.server_description, creative_mode: updates.creativeMode === 'on', enable_damage: updates.enableDamage !== 'off', enable_pvp: updates.enablePvp !== 'off', server_announce: updates.serverAnnounce === 'on' }; await ConfigParser.writeWorldConfig(configPath, updatedConfig); res.redirect(`/worlds/${worldName}?updated=true`); } catch (error) { console.error('Error updating world:', error); res.status(500).json({ error: 'Failed to update world' }); } }); // Delete world router.post('/:worldName/delete', async (req, res) => { try { const { worldName } = req.params; if (!paths.isValidWorldName(worldName)) { return res.status(400).json({ error: 'Invalid world name' }); } const worldPath = paths.getWorldPath(worldName); try { await fs.access(worldPath); } catch { return res.status(404).json({ error: 'World not found' }); } // Deletion confirmed by frontend confirmation dialog await fs.rm(worldPath, { recursive: true, force: true }); res.redirect('/worlds?deleted=' + encodeURIComponent(worldName)); } catch (error) { console.error('Error deleting world:', error); res.status(500).json({ error: 'Failed to delete world' }); } }); module.exports = router;