Generalize game ID normalization for all _game suffixes

Previously the system only handled the specific case of minetest_game -> minetest.
Based on Luanti developer feedback that "_game is removed from IDs as part of
normalisation for all game IDs not just MTG", this change generalizes the pattern.

## Changes Made

### Enhanced Game ID Normalization
- `mapToActualGameId()`: Now automatically removes "_game" suffix from any game ID
- `mapGameIdForWorldCreation()`: Generalizes suffix removal for world.mt files
- `mapInternalGameIdToDirectory()`: Enhanced to dynamically check filesystem for directories

### Backwards Compatibility
- All existing minetest_game -> minetest mappings continue to work
- Now also handles any other games with _game suffix (e.g., survival_game -> survival)
- Original EJS templates already compatible via world.gameTitle || world.gameid pattern

### Technical Implementation
- Replaces hardcoded mapping tables with general suffix detection
- Maintains proper fallback behavior for games without _game suffix
- Filesystem-aware directory resolution for reverse mapping

🤖 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 11:26:31 -06:00
parent 88ebb4c603
commit def0a66028

View File

@@ -182,33 +182,72 @@ class LuantiPaths {
mapToActualGameId(directoryName) { mapToActualGameId(directoryName) {
// Map directory names to the actual game IDs that Luanti recognizes // Map directory names to the actual game IDs that Luanti recognizes
// For most cases, the directory name IS the game ID // Luanti normalizes game IDs by removing "_game" suffix from all games
const gameIdMap = { // See https://github.com/luanti-org/luanti/blob/c9d4c33174c87ede1f49c5fe5e8e49a784798eb6/src/content/subgames.cpp#L21
// Luanti internal alias mapping - see https://github.com/luanti-org/luanti/blob/c9d4c33174c87ede1f49c5fe5e8e49a784798eb6/src/content/subgames.cpp#L21
'minetest_game': 'minetest',
};
return gameIdMap[directoryName] || directoryName; // Remove "_game" suffix if present
if (directoryName.endsWith('_game')) {
return directoryName.slice(0, -5); // Remove last 5 characters ("_game")
}
return directoryName;
} }
mapGameIdForWorldCreation(gameId) { mapGameIdForWorldCreation(gameId) {
// When creating worlds, map game IDs that Luanti expects to use different IDs internally // When creating worlds, map game IDs that Luanti expects to use different IDs internally
// This is the reverse of directory-based detection - we're setting what goes in world.mt // This is the reverse of directory-based detection - we're setting what goes in world.mt
const worldGameIdMap = { // Luanti normalizes all game IDs by removing "_game" suffix
'minetest_game': 'minetest', // Luanti expects 'minetest' in world.mt even for minetest_game
};
return worldGameIdMap[gameId] || gameId; // Remove "_game" suffix if present
if (gameId.endsWith('_game')) {
return gameId.slice(0, -5); // Remove last 5 characters ("_game")
} }
mapInternalGameIdToDirectory(internalGameId) { return gameId;
// Reverse mapping: convert internal game ID back to directory name for display/reference }
// This helps when we read a world.mt with "minetest" but want to show "Minetest Game"
const reverseGameIdMap = {
'minetest': 'minetest_game', // world.mt has 'minetest' but directory is 'minetest_game'
};
return reverseGameIdMap[internalGameId] || internalGameId; async mapInternalGameIdToDirectory(internalGameId) {
// Reverse mapping: convert internal game ID back to directory name for display/reference
// This helps when we read a world.mt with normalized game ID but want to show directory name
// Since Luanti normalizes by removing "_game" suffix, we need to check if a directory
// with "_game" suffix exists for this normalized ID
const fs = require('fs').promises;
const path = require('path');
// Check both system and user games directories
const gameDirs = [this.getGamesPath()];
if (this.getSystemGamesPath() && this.getSystemGamesPath() !== this.getGamesPath()) {
gameDirs.push(this.getSystemGamesPath());
}
for (const gameDir of gameDirs) {
try {
// First check if the internalGameId as-is exists as a directory
const directPath = path.join(gameDir, internalGameId);
const stat = await fs.stat(directPath);
if (stat.isDirectory()) {
return internalGameId;
}
} catch (error) {
// Directory doesn't exist, try with "_game" suffix
}
try {
// Check if there's a directory with "_game" suffix
const gameIdWithSuffix = internalGameId + '_game';
const suffixPath = path.join(gameDir, gameIdWithSuffix);
const stat = await fs.stat(suffixPath);
if (stat.isDirectory()) {
return gameIdWithSuffix;
}
} catch (error) {
// Directory doesn't exist either
}
}
// Fallback: return the original ID
return internalGameId;
} }
async getInstalledGames() { async getInstalledGames() {