Implement configuration-based mod management and fix navigation spacing

## Major Features Added

### Configuration-Based Mod Management
- Implement proper Luanti mod system using load_mod_* entries in world.mt
- Add mod enable/disable via configuration instead of file copying
- Support both global mods (config-enabled) and world mods (physically installed)
- Clear UI distinction with badges: "Global (Enabled)", "World Copy", "Missing"
- Automatic registry verification to sync database with filesystem state

### Game ID Alias System
- Fix minetest_game/minetest technical debt with proper alias mapping
- Map minetest_game → minetest for world.mt files (matches Luanti internal behavior)
- Reference: c9d4c33174/src/content/subgames.cpp (L21)

### Navigation Improvements
- Fix navigation menu spacing and text overflow issues
- Change "Configuration" to "Config" for better fit
- Implement responsive font sizing with clamp() for better scaling
- Even distribution of nav buttons across full width

### Package Registry Enhancements
- Add verifyAndCleanRegistry() to automatically remove stale package entries
- Periodic verification (every 5 minutes) to keep registry in sync with filesystem
- Fix "already installed" errors for manually deleted packages
- Integration across dashboard, ContentDB, and installation workflows

## Technical Improvements

### Mod System Architecture
- Enhanced ConfigParser to handle load_mod_* entries in world.mt files
- Support for both configuration-based and file-based mod installations
- Proper mod type detection and management workflows
- Updated world details to show comprehensive mod information

### UI/UX Enhancements
- Responsive navigation with proper text scaling
- Improved mod management interface with clear action buttons
- Better visual hierarchy and status indicators
- Enhanced error handling and user feedback

### Code Quality
- Clean up gitignore to properly exclude runtime files
- Add package-lock.json for consistent dependency management
- Remove excess runtime database and log files
- Add .claude/ directory to gitignore

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Nathan Schneider
2025-09-08 10:31:08 -06:00
parent 2d3b1166fe
commit 8aadab1b50
12 changed files with 4047 additions and 92 deletions

View File

@@ -33,11 +33,17 @@ router.get('/', async (req, res) => {
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();
// Only try to read if the database file already exists and has content
if (fs.existsSync(playersDbPath)) {
const stats = fs.statSync(playersDbPath);
if (stats.size > 0) {
const db = new sqlite3.Database(playersDbPath, sqlite3.OPEN_READONLY);
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) {}
const gameid = config.gameid || 'minetest_game';
@@ -131,18 +137,29 @@ router.post('/create', async (req, res) => {
});
} catch {}
console.log('Starting world creation for:', name, 'with gameid:', gameid);
console.log('Creating world:', name, 'with gameid:', gameid);
// Start world creation asynchronously and respond immediately
serverManager.createWorld(name, gameid || 'minetest_game')
// Get the default game if none specified
let finalGameId = gameid;
if (!finalGameId) {
try {
const games = await paths.getInstalledGames();
finalGameId = games.length > 0 ? games[0].name : 'minetest_game';
} catch (error) {
finalGameId = 'minetest_game';
}
}
// Start world creation and redirect immediately with creating status
serverManager.createWorld(name, finalGameId)
.then(() => {
console.log('Successfully created world using Luanti:', name);
console.log('Successfully created world:', name);
// Notify clients via WebSocket
const io = req.app.get('socketio');
if (io) {
io.emit('worldCreated', {
worldName: name,
gameId: gameid || 'minetest_game',
gameId: finalGameId,
success: true
});
}
@@ -154,7 +171,7 @@ router.post('/create', async (req, res) => {
if (io) {
io.emit('worldCreated', {
worldName: name,
gameId: gameid || 'minetest_game',
gameId: finalGameId,
success: false,
error: error.message
});
@@ -207,6 +224,40 @@ router.get('/:worldName', async (req, res) => {
} catch {}
let enabledMods = [];
// Get configuration-enabled mods (from global mods directory)
if (config.enabled_mods) {
for (const [modName, enabled] of Object.entries(config.enabled_mods)) {
if (enabled) {
try {
const globalModPath = paths.getModPath(modName);
const globalModConfigPath = paths.getModConfigPath(modName);
await fs.access(globalModPath);
const modConfig = await ConfigParser.parseModConfig(globalModConfigPath);
enabledMods.push({
name: modName,
title: modConfig.title || modName,
description: modConfig.description || '',
author: modConfig.author || '',
location: 'global-enabled',
path: globalModPath
});
} catch {
// Global mod not found, but still enabled in config - show as missing
enabledMods.push({
name: modName,
title: modName,
description: 'Missing global mod',
author: '',
location: 'global-missing',
path: null
});
}
}
}
}
// Get physically installed world mods
try {
const worldModsPath = paths.getWorldModsPath(worldName);
const modDirs = await fs.readdir(worldModsPath);
@@ -219,7 +270,8 @@ router.get('/:worldName', async (req, res) => {
title: modConfig.title || modDir,
description: modConfig.description || '',
author: modConfig.author || '',
location: 'world'
location: 'world-installed',
path: path.join(worldModsPath, modDir)
});
} catch {}
}