diff --git a/gsm-backend/services/rcon.js b/gsm-backend/services/rcon.js index 1553640..ed8aba6 100644 --- a/gsm-backend/services/rcon.js +++ b/gsm-backend/services/rcon.js @@ -1,4 +1,5 @@ import { Rcon } from 'rcon-client'; +import { getHytalePlayers } from './ssh.js'; const rconConnections = new Map(); const playerCache = new Map(); @@ -109,6 +110,10 @@ export async function getPlayers(server) { // Use REST API instead of RCON for Palworld const data = await getPalworldPlayers(server); result = { online: data.players?.length || 0, max: null }; + } else if (server.type === 'hytale') { + // Use log parsing for Hytale (no RCON support) + const data = await getHytalePlayers(server); + result = { online: data.online, max: 32 }; } playerCache.set(cacheKey, { data: result, time: Date.now() }); @@ -171,6 +176,10 @@ export async function getPlayerList(server) { // Use REST API instead of RCON for Palworld const data = await getPalworldPlayers(server); players = (data.players || []).map(p => p.name); + } else if (server.type === 'hytale') { + // Use log parsing for Hytale (no RCON support) + const data = await getHytalePlayers(server); + players = data.players || []; } const result = { players }; diff --git a/gsm-backend/services/ssh.js b/gsm-backend/services/ssh.js index 2d42582..ff1f778 100644 --- a/gsm-backend/services/ssh.js +++ b/gsm-backend/services/ssh.js @@ -643,8 +643,50 @@ export async function writeOpenTTDConfig(server, content) { return true; } -// Hytale Config Management +// ============ HYTALE FUNCTIONS ============ const HYTALE_CONFIG_PATH = "/opt/hytale/Server/config.json"; +const HYTALE_LOGS_PATH = "/opt/hytale/Server/logs"; + +// Get Hytale players by parsing server logs +export async function getHytalePlayers(server) { + const ssh = await getConnection(server.host, server.sshUser); + + // Find the most recent log file + const logFileResult = await ssh.execCommand(`ls -t ${HYTALE_LOGS_PATH}/*.log 2>/dev/null | head -1`); + if (!logFileResult.stdout.trim()) { + return { online: 0, players: [] }; + } + + const logFile = logFileResult.stdout.trim(); + + // Parse log for player joins and disconnects + // Join pattern: [Universe|P] Adding player 'PlayerName (UUID) + // Disconnect pattern: Search for connection closed or player removed + const result = await ssh.execCommand(`grep -E "\\[Universe\\|P\\] Adding player|Removing player|Connection.*closed|disconnect" ${logFile} 2>/dev/null | tail -200`); + + const players = new Map(); // UUID -> PlayerName + + const lines = result.stdout.split('\n'); + for (const line of lines) { + // Check for player join + const joinMatch = line.match(/\[Universe\|P\] Adding player '([^']+) \(([a-f0-9-]+)\)/i); + if (joinMatch) { + const playerName = joinMatch[1]; + const uuid = joinMatch[2]; + players.set(uuid, playerName); + continue; + } + + // Check for player disconnect (various patterns) + const disconnectMatch = line.match(/Removing player.*\(([a-f0-9-]+)\)/i); + if (disconnectMatch) { + players.delete(disconnectMatch[1]); + } + } + + const playerList = Array.from(players.values()); + return { online: playerList.length, players: playerList }; +} export async function readHytaleConfig(server) { const ssh = await getConnection(server.host, server.sshUser);