Files
red-valley/nui-simulator/server.js

217 lines
7.3 KiB
JavaScript
Raw Normal View History

/**
* Red Valley NUI Simulator Server
* Scans a FiveM server folder for NUI resources and serves them for browser testing.
*
* Usage: node server.js [path-to-server-folder]
* Default path: parent directory (../)
*/
const express = require('express');
const path = require('path');
const fs = require('fs');
const { exec } = require('child_process');
// ============ CONFIG ============
const PORT = 3200;
const args = process.argv.slice(2);
const SERVER_PATH = args.find(a => !a.startsWith('--')) || path.resolve(__dirname, '..');
const RESOURCES_PATH = path.join(SERVER_PATH, 'resources');
const AUTO_OPEN = args.includes('--open');
// ============ NUI SCANNER ============
/**
* Recursively find all fxmanifest.lua / __resource.lua files
*/
function findManifests(dir, results = []) {
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip node_modules, .git, stream folders
if (['node_modules', '.git', 'stream', 'streams'].includes(entry.name)) continue;
findManifests(fullPath, results);
} else if (entry.name === 'fxmanifest.lua' || entry.name === '__resource.lua') {
results.push(fullPath);
}
}
} catch (e) {
// Permission denied or similar - skip
}
return results;
}
/**
* Parse a fxmanifest.lua to extract ui_page directive
*/
function parseManifest(manifestPath) {
try {
const content = fs.readFileSync(manifestPath, 'utf-8');
// Match ui_page 'path' or ui_page "path" or ui_page('path') etc.
const patterns = [
/ui_page\s*[\(\{]?\s*['"]([^'"]+)['"]\s*[\)\}]?/i,
/ui_page\s+['"]([^'"]+)['"]/i,
];
for (const pattern of patterns) {
const match = content.match(pattern);
if (match) {
return match[1];
}
}
} catch (e) {
// Can't read file
}
return null;
}
/**
* Extract resource name from path
*/
function getResourceName(manifestPath) {
const dir = path.dirname(manifestPath);
return path.basename(dir);
}
/**
* Get category/tags based on folder structure
*/
function categorize(relativePath) {
const rp = relativePath.toLowerCase();
if (rp.includes('[core]')) return { tag: 'core', color: '#c084fc' };
if (rp.includes('[depends]')) return { tag: 'depends', color: '#60a5fa' };
if (rp.includes('[addons]')) return { tag: 'addon', color: '#f59e0b' };
if (rp.includes('[jobs]')) return { tag: 'job', color: '#22c55e' };
if (rp.includes('[auth]')) return { tag: 'auth', color: '#f472b6' };
if (rp.includes('[ui]')) return { tag: 'ui', color: '#38bdf8' };
if (rp.includes('[legal]')) return { tag: 'legal', color: '#34d399' };
return { tag: 'other', color: '#8b8d94' };
}
/**
* Auto-detect icon based on resource name
*/
function getIcon(name) {
const icons = {
'phone': '📱', 'radio': '📻', 'hud': '📊', 'menu': '📋',
'target': '🎯', 'input': '📝', 'notify': '🔔', 'sound': '🔊',
'progress': '⏳', 'banking': '🏦', 'casino': '🎰', 'dice': '🎲',
'admin': '⚙️', 'ambulance': '🚑', 'police': '🚔', 'mechanic': '🔧',
'dispatch': '📻', 'boss': '👔', 'character': '👤', 'license': '🔒',
'job': '💼', 'garbage': '🗑️', 'lumber': '🪓', 'miner': '⛏️',
'oil': '🛢️', 'bus': '🚌', 'car': '🚗', 'tuning': '🏎️',
'driver': '🏫', 'anim': '💃', 'dyno': '📈', 'smart': '📱',
};
const nameLower = name.toLowerCase();
for (const [key, icon] of Object.entries(icons)) {
if (nameLower.includes(key)) return icon;
}
return '📦';
}
/**
* Scan all resources and return NUI info
*/
function scanNUIs() {
console.log(`\n 🔍 Scanning: ${RESOURCES_PATH}\n`);
if (!fs.existsSync(RESOURCES_PATH)) {
console.error(` ❌ Resources folder not found: ${RESOURCES_PATH}`);
process.exit(1);
}
const manifests = findManifests(RESOURCES_PATH);
console.log(` 📄 Found ${manifests.length} manifest files`);
const nuis = [];
for (const mf of manifests) {
const uiPage = parseManifest(mf);
if (uiPage) {
const resourceDir = path.dirname(mf);
const name = getResourceName(mf);
const relativePath = path.relative(RESOURCES_PATH, resourceDir).replace(/\\/g, '/');
const uiFullPath = path.join(resourceDir, uiPage);
const category = categorize(relativePath);
// Check if the UI file actually exists
const exists = fs.existsSync(uiFullPath);
nuis.push({
name,
path: relativePath,
uiPage,
uiFullPath: path.relative(RESOURCES_PATH, uiFullPath).replace(/\\/g, '/'),
exists,
icon: getIcon(name),
...category,
});
}
}
// Sort: existing first, then alphabetically
nuis.sort((a, b) => {
if (a.exists !== b.exists) return b.exists - a.exists;
return a.name.localeCompare(b.name);
});
console.log(` 🎨 Found ${nuis.length} NUI resources\n`);
nuis.forEach(n => {
const status = n.exists ? '✅' : '⚠️';
console.log(` ${status} ${n.icon} ${n.name}${n.uiPage}`);
});
console.log('');
return nuis;
}
// ============ SERVER ============
const app = express();
// Scan NUIs at startup
const discoveredNUIs = scanNUIs();
// API: return discovered NUIs
app.get('/api/nuis', (req, res) => {
res.json(discoveredNUIs);
});
// API: rescan
app.get('/api/rescan', (req, res) => {
const fresh = scanNUIs();
discoveredNUIs.length = 0;
discoveredNUIs.push(...fresh);
res.json(fresh);
});
// Serve NUI files from resources folder
app.use('/resources', express.static(RESOURCES_PATH));
// Serve mock files from docs folder
app.use('/mocks', express.static(path.join(SERVER_PATH, 'docs')));
// Serve the simulator UI
app.use(express.static(path.join(__dirname, 'public')));
// Start
app.listen(PORT, () => {
const url = `http://localhost:${PORT}`;
console.log(` ╔═══════════════════════════════════════════════════╗`);
console.log(` ║ ║`);
console.log(` ║ 🎮 Red Valley — NUI Simulator ║`);
console.log(` ║ ║`);
console.log(` ║ Server: ${url}`);
console.log(` ║ Resources: ${discoveredNUIs.length} NUIs discovered ║`);
console.log(` ║ ║`);
console.log(` ╚═══════════════════════════════════════════════════╝\n`);
// Auto-open browser
if (AUTO_OPEN) {
const cmd = process.platform === 'win32' ? 'start' : 'open';
exec(`${cmd} ${url}`);
}
});