const path = require('path'); const os = require('os'); const fs = require('fs'); const appConfig = require('./app-config'); class LuantiPaths { constructor() { // Initialize with default, will be updated when app config loads this.setDataDirectory(this.getDefaultDataDirectory()); } async initialize() { // Load app config and update data directory await appConfig.load(); const configuredDataDir = appConfig.getDataDirectory(); this.setDataDirectory(configuredDataDir); } getDefaultDataDirectory() { // Check for common Luanti data directories const homeDir = os.homedir(); const possibleDirs = [ path.join(homeDir, '.luanti'), path.join(homeDir, '.minetest') ]; // Use the first one that exists, or default to .minetest for (const dir of possibleDirs) { if (fs.existsSync(dir)) { return dir; } } return path.join(homeDir, '.minetest'); } setDataDirectory(dataDir) { this.minetestDir = path.resolve(dataDir); this.worldsDir = path.join(this.minetestDir, 'worlds'); this.modsDir = path.join(this.minetestDir, 'mods'); this.gamesDir = path.join(this.minetestDir, 'games'); this.texturesDir = path.join(this.minetestDir, 'textures'); this.configFile = path.join(this.minetestDir, 'minetest.conf'); this.debugFile = path.join(this.minetestDir, 'debug.txt'); } getDataDirectory() { return this.minetestDir; } getWorldPath(worldName) { return path.join(this.worldsDir, worldName); } getWorldConfigPath(worldName) { return path.join(this.getWorldPath(worldName), 'world.mt'); } getWorldModsPath(worldName) { return path.join(this.getWorldPath(worldName), 'worldmods'); } getModPath(modName) { return path.join(this.modsDir, modName); } getModConfigPath(modName) { return path.join(this.getModPath(modName), 'mod.conf'); } getGamePath(gameName) { return path.join(this.gamesDir, gameName); } getGameConfigPath(gameName) { return path.join(this.getGamePath(gameName), 'game.conf'); } ensureDirectories() { const dirs = [this.minetestDir, this.worldsDir, this.modsDir, this.gamesDir, this.texturesDir]; dirs.forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } }); } isValidWorldName(name) { if (!name || typeof name !== 'string') return false; return /^[a-zA-Z0-9_-]+$/.test(name) && name.length >= 3 && name.length <= 50; } isValidModName(name) { if (!name || typeof name !== 'string') return false; return /^[a-zA-Z0-9_-]+$/.test(name) && name.length <= 50; } isPathSafe(targetPath) { const resolvedPath = path.resolve(targetPath); return resolvedPath.startsWith(path.resolve(this.minetestDir)); } mapToActualGameId(directoryName) { // Map directory names to the actual game IDs that Luanti recognizes // For most cases, the directory name IS the game ID const gameIdMap = { // Only add mappings here if you're certain they're needed // 'minetest_game': 'minetest', // This mapping was incorrect }; return gameIdMap[directoryName] || directoryName; } async getInstalledGames() { const games = []; const possibleGameDirs = [ this.gamesDir, // User games directory '/usr/share/luanti/games', // System games directory '/usr/share/minetest/games', // Legacy system games directory path.join(process.env.HOME || '/root', '.minetest/games'), // Explicit user path path.join(process.env.HOME || '/root', '.luanti/games') // New user path ]; for (const gameDir of possibleGameDirs) { try { const exists = fs.existsSync(gameDir); if (!exists) continue; const gameDirs = fs.readdirSync(gameDir); for (const gameName of gameDirs) { const possibleConfigPaths = [ path.join(gameDir, gameName, 'game.conf'), path.join(gameDir, gameName, gameName, 'game.conf') // Handle nested structure ]; for (const gameConfigPath of possibleConfigPaths) { try { if (fs.existsSync(gameConfigPath)) { const ConfigParser = require('./config-parser'); const gameConfig = await ConfigParser.parseGameConfig(gameConfigPath); // Map directory names to actual game IDs that Luanti recognizes const actualGameId = this.mapToActualGameId(gameName); // Check if we already have this game (avoid duplicates by game ID, title, and resolved path) const resolvedPath = fs.realpathSync(path.dirname(gameConfigPath)); const existingGame = games.find(g => g.name === actualGameId || (g.title === (gameConfig.title || gameConfig.name || gameName) && g.resolvedPath === resolvedPath) ); if (!existingGame) { games.push({ name: actualGameId, // Use the ID that Luanti recognizes directoryName: gameName, // Keep original for path resolution title: gameConfig.title || gameConfig.name || gameName, description: gameConfig.description || '', author: gameConfig.author || '', path: path.dirname(gameConfigPath), resolvedPath: resolvedPath, isSystemGame: !gameDir.includes(this.minetestDir) }); } break; // Found valid config, stop checking other paths } } catch (gameError) { // Skip invalid games console.warn(`Invalid game at ${gameConfigPath}:`, gameError.message); } } } } catch (dirError) { // Skip directories that can't be read continue; } } // Sort games: system games first, then minetest_game first, then alphabetically games.sort((a, b) => { if (a.isSystemGame !== b.isSystemGame) { return a.isSystemGame ? -1 : 1; } // Put minetest_game first as it's the default if (a.name === 'minetest_game') return -1; if (b.name === 'minetest_game') return 1; return a.title.localeCompare(b.title); }); return games; } } module.exports = new LuantiPaths();