217 lines
7.3 KiB
JavaScript
217 lines
7.3 KiB
JavaScript
|
|
/**
|
||
|
|
* 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}`);
|
||
|
|
}
|
||
|
|
});
|