All checks were successful
Deploy GSM / deploy (push) Successful in 41s
- Add ShowPlayers RCON command handling for Palworld servers - Update SSH access documentation with jump host instructions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
175 lines
5.9 KiB
JavaScript
175 lines
5.9 KiB
JavaScript
import { Rcon } from 'rcon-client';
|
|
|
|
const rconConnections = new Map();
|
|
const playerCache = new Map();
|
|
const CACHE_TTL = 30000; // 30 seconds
|
|
|
|
async function getConnection(server) {
|
|
const key = `${server.host}:${server.rconPort}`;
|
|
|
|
if (rconConnections.has(key)) {
|
|
const conn = rconConnections.get(key);
|
|
if (conn.authenticated) {
|
|
return conn;
|
|
}
|
|
rconConnections.delete(key);
|
|
}
|
|
|
|
const rcon = await Rcon.connect({
|
|
host: server.host,
|
|
port: server.rconPort,
|
|
password: server.rconPassword,
|
|
timeout: 5000
|
|
});
|
|
|
|
rcon.on('error', (err) => {
|
|
console.error(`RCON error for ${key}:`, err.message);
|
|
rconConnections.delete(key);
|
|
});
|
|
|
|
rcon.on('end', () => {
|
|
rconConnections.delete(key);
|
|
});
|
|
|
|
// Handle socket errors to prevent crash
|
|
if (rcon.socket) {
|
|
rcon.socket.on('error', (err) => {
|
|
console.error(`RCON socket error for ${key}:`, err.message);
|
|
rconConnections.delete(key);
|
|
});
|
|
}
|
|
|
|
rconConnections.set(key, rcon);
|
|
return rcon;
|
|
}
|
|
|
|
export async function sendRconCommand(server, command) {
|
|
if (!server.rconPassword) {
|
|
throw new Error('RCON password not configured');
|
|
}
|
|
|
|
const rcon = await getConnection(server);
|
|
return await rcon.send(command);
|
|
}
|
|
|
|
export async function getPlayers(server) {
|
|
const cacheKey = `${server.id}-count`;
|
|
const cached = playerCache.get(cacheKey);
|
|
|
|
if (cached && Date.now() - cached.time < CACHE_TTL) {
|
|
return cached.data;
|
|
}
|
|
|
|
try {
|
|
let result = { online: 0, max: null };
|
|
|
|
if (server.type === 'minecraft') {
|
|
const response = await sendRconCommand(server, 'list');
|
|
const match = response.match(/There are (\d+) of a max of (\d+) players online/);
|
|
if (match) {
|
|
result = { online: parseInt(match[1]), max: parseInt(match[2]) };
|
|
}
|
|
} else if (server.type === 'factorio') {
|
|
const response = await sendRconCommand(server, '/players online count');
|
|
const match = response.match(/(\d+)/);
|
|
if (match) {
|
|
result = { online: parseInt(match[1]), max: null };
|
|
}
|
|
} else if (server.type === 'zomboid') {
|
|
const response = await sendRconCommand(server, 'players');
|
|
// Format: "Players connected (X):" or "Players connected (X): Player1, Player2"
|
|
const match = response.match(/Players connected \((\d+)\)/);
|
|
if (match) {
|
|
result = { online: parseInt(match[1]), max: null };
|
|
}
|
|
} else if (server.type === 'vrising') {
|
|
const response = await sendRconCommand(server, 'listusers');
|
|
// Count lines that contain player info
|
|
const lines = response.split('\n').filter(l => l.trim() && !l.includes('listusers'));
|
|
result = { online: lines.length, max: null };
|
|
} else if (server.type === 'palworld') {
|
|
const response = await sendRconCommand(server, 'ShowPlayers');
|
|
// Format: "name,playeruid,steamid\nPlayer1,123,765...\nPlayer2,456,765..."
|
|
const lines = response.split('\n').filter(l => l.trim() && !l.toLowerCase().startsWith('name,'));
|
|
result = { online: lines.length, max: null };
|
|
}
|
|
|
|
playerCache.set(cacheKey, { data: result, time: Date.now() });
|
|
return result;
|
|
} catch (err) {
|
|
console.error(`Failed to get players for ${server.id}:`, err.message);
|
|
// Clear cache on error - server might be offline
|
|
playerCache.delete(cacheKey);
|
|
return { online: 0, max: null };
|
|
}
|
|
}
|
|
|
|
export async function getPlayerList(server) {
|
|
const cacheKey = `${server.id}-list`;
|
|
const cached = playerCache.get(cacheKey);
|
|
|
|
if (cached && Date.now() - cached.time < CACHE_TTL) {
|
|
return cached.data;
|
|
}
|
|
|
|
try {
|
|
let players = [];
|
|
|
|
if (server.type === 'minecraft') {
|
|
const response = await sendRconCommand(server, 'list');
|
|
// Format: "There are X of a max of Y players online: Player1, Player2, Player3"
|
|
const colonIndex = response.indexOf(':');
|
|
if (colonIndex !== -1) {
|
|
const playerPart = response.substring(colonIndex + 1).trim();
|
|
if (playerPart) {
|
|
players = playerPart.split(',').map(p => p.trim()).filter(p => p);
|
|
}
|
|
}
|
|
} else if (server.type === 'factorio') {
|
|
const response = await sendRconCommand(server, '/players online');
|
|
// Format: "Online players (X):\n player1 (online)\n player2 (online)"
|
|
const lines = response.split('\n');
|
|
for (const line of lines) {
|
|
const match = line.match(/^\s*(\S+)\s*\(online\)/);
|
|
if (match) {
|
|
players.push(match[1]);
|
|
}
|
|
}
|
|
} else if (server.type === 'zomboid') {
|
|
const response = await sendRconCommand(server, 'players');
|
|
// Format: "Players connected (X): \n-Player1\n-Player2\n-Player3"
|
|
const lines = response.split('\n');
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (trimmed.startsWith('-')) {
|
|
players.push(trimmed.substring(1));
|
|
}
|
|
}
|
|
} else if (server.type === 'vrising') {
|
|
const response = await sendRconCommand(server, 'listusers');
|
|
// Parse player names from listusers output
|
|
const lines = response.split('\n').filter(l => l.trim() && !l.includes('listusers'));
|
|
players = lines.map(l => l.trim()).filter(p => p);
|
|
} else if (server.type === 'palworld') {
|
|
const response = await sendRconCommand(server, 'ShowPlayers');
|
|
// Format: "name,playeruid,steamid\nPlayer1,123,765...\nPlayer2,456,765..."
|
|
const lines = response.split('\n').filter(l => l.trim() && !l.toLowerCase().startsWith('name,'));
|
|
for (const line of lines) {
|
|
const parts = line.split(',');
|
|
if (parts.length >= 1 && parts[0].trim()) {
|
|
players.push(parts[0].trim());
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = { players };
|
|
playerCache.set(cacheKey, { data: result, time: Date.now() });
|
|
return result;
|
|
} catch (err) {
|
|
console.error(`Failed to get player list for ${server.id}:`, err.message);
|
|
// Clear cache on error - server might be offline
|
|
playerCache.delete(cacheKey);
|
|
return { players: [] };
|
|
}
|
|
}
|