A modern web interface for Luanti (Minetest) server management with ContentDB integration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
194 lines
6.3 KiB
JavaScript
194 lines
6.3 KiB
JavaScript
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(); |