Add Terraria player detection via log parsing
All checks were successful
Deploy GSM / deploy (push) Successful in 27s
All checks were successful
Deploy GSM / deploy (push) Successful in 27s
- Add getTerrariaPlayers function in ssh.js for PM2 log parsing - Support German and English join/leave messages - Update rcon.js to use Terraria log parsing - Add Terraria to player fetch conditions in servers.js - Update autoshutdown.js and discordBot.js for Terraria support - Update config path to tModLoader directory - Add global error handlers in server.js - Update CLAUDE.md with deployment rules and Terraria info Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
12
CLAUDE.md
12
CLAUDE.md
@@ -17,7 +17,7 @@ The homelab consists of:
|
||||
- **V Rising Server (192.168.2.52)**: Dedicated server (LXC)
|
||||
- **Palworld Server (192.168.2.53)**: Dedicated server with systemd (LXC)
|
||||
- **Project Zomboid Server (10.0.30.66)**: Dedicated server (external VM)
|
||||
- **Terraria Server (10.0.30.202)**: Vanilla server mit PM2 (external VM, VPN)
|
||||
- **Terraria Server (10.0.30.202)**: tModLoader mit Calamity Mod, PM2 (external VM, VPN)
|
||||
- **Hytale Server (10.0.30.204)**: Dedicated server mit tmux (external VM, VPN)
|
||||
|
||||
## Key Technical Details
|
||||
@@ -61,11 +61,21 @@ ssh root@192.168.2.30 'curl -X POST http://localhost:3000/api/servers/discord/in
|
||||
|
||||
**Server ohne RCON (Spielererkennung via Log-Parsing)**:
|
||||
- Hytale: Spieler werden über Server-Logs erkannt (`[World|*] Player joined` / `[PlayerSystems] Removing player`)
|
||||
- Terraria: Spieler werden über PM2-Logs erkannt (`ist beigetreten` / `hat das Spiel verlassen`)
|
||||
- Bei neuen Servern ohne RCON: `server.type === 'serverid'` zu folgenden Dateien hinzufügen:
|
||||
- `services/autoshutdown.js` (Zeile ~50)
|
||||
- `services/discordBot.js` (fetchServerStatuses, Zeile ~379)
|
||||
- `routes/servers.js` (zwei Stellen, suche nach `rconPassword || server.type`)
|
||||
|
||||
## Deployment
|
||||
|
||||
**WICHTIG: NIEMALS per SCP deployen!** Alle Änderungen am GSM-Code müssen:
|
||||
1. Lokal committed werden
|
||||
2. Auf GitHub gepusht werden
|
||||
3. Das CI/CD-System deployt automatisch auf den Server
|
||||
|
||||
Kein manuelles Kopieren von Dateien per SCP, rsync oder ähnlichem!
|
||||
|
||||
## Language Note
|
||||
|
||||
Documentation is written in German.
|
||||
|
||||
@@ -348,8 +348,8 @@ router.get('/', optionalAuth, async (req, res) => {
|
||||
getCurrentMetrics(server.id).catch(() => ({
|
||||
cpu: 0, cpuCores: 1, memory: 0, memoryUsed: 0, memoryTotal: 0, uptime: 0
|
||||
})),
|
||||
(server.rconPassword || server.type === 'hytale') ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null },
|
||||
(server.rconPassword || server.type === 'hytale') ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] },
|
||||
(server.rconPassword || server.type === 'hytale' || server.type === 'terraria') ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null },
|
||||
(server.rconPassword || server.type === 'hytale' || server.type === 'terraria') ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] },
|
||||
getProcessUptime(server).catch(() => 0)
|
||||
]);
|
||||
|
||||
@@ -586,8 +586,8 @@ router.get('/:id', authenticateToken, rejectGuest, async (req, res) => {
|
||||
getCurrentMetrics(server.id).catch(() => ({
|
||||
cpu: 0, cpuCores: 1, memory: 0, memoryUsed: 0, memoryTotal: 0, uptime: 0
|
||||
})),
|
||||
server.rconPassword ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null },
|
||||
server.rconPassword ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] },
|
||||
(server.rconPassword || server.type === 'hytale' || server.type === 'terraria') ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null },
|
||||
(server.rconPassword || server.type === 'hytale' || server.type === 'terraria') ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] },
|
||||
getProcessUptime(server).catch(() => 0)
|
||||
]);
|
||||
|
||||
|
||||
@@ -9,6 +9,16 @@ import { initDiscordBot } from './services/discordBot.js';
|
||||
|
||||
config();
|
||||
|
||||
// Global error handlers to prevent crashes
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('[FATAL] Uncaught Exception:', err.message);
|
||||
console.error(err.stack);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[ERROR] Unhandled Promise Rejection:', reason);
|
||||
});
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ async function checkServers() {
|
||||
// Get player count
|
||||
// Some servers use RCON, others use log parsing (like Hytale)
|
||||
let playerCount = 0;
|
||||
if (server.rconPassword || server.type === 'hytale') {
|
||||
if (server.rconPassword || server.type === 'hytale' || server.type === 'terraria') {
|
||||
const players = await getPlayers(server);
|
||||
playerCount = players.online || 0;
|
||||
}
|
||||
|
||||
@@ -376,7 +376,7 @@ async function fetchServerStatuses() {
|
||||
let players = { online: 0, max: null };
|
||||
let playerList = { players: [] };
|
||||
|
||||
if (running && (server.rconPassword || server.type === 'hytale')) {
|
||||
if (running && (server.rconPassword || server.type === 'hytale' || server.type === 'terraria')) {
|
||||
try {
|
||||
players = await getPlayers(server);
|
||||
playerList = await getPlayerList(server);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Rcon } from 'rcon-client';
|
||||
import { getHytalePlayers } from './ssh.js';
|
||||
import { getHytalePlayers, getTerrariaPlayers } from './ssh.js';
|
||||
|
||||
const rconConnections = new Map();
|
||||
const playerCache = new Map();
|
||||
@@ -114,6 +114,10 @@ export async function getPlayers(server) {
|
||||
// Use log parsing for Hytale (no RCON support)
|
||||
const data = await getHytalePlayers(server);
|
||||
result = { online: data.online, max: 32 };
|
||||
} else if (server.type === 'terraria') {
|
||||
// Use log parsing for Terraria (no RCON support)
|
||||
const data = await getTerrariaPlayers(server);
|
||||
result = { online: data.online, max: 8 };
|
||||
}
|
||||
|
||||
playerCache.set(cacheKey, { data: result, time: Date.now() });
|
||||
@@ -180,6 +184,10 @@ export async function getPlayerList(server) {
|
||||
// Use log parsing for Hytale (no RCON support)
|
||||
const data = await getHytalePlayers(server);
|
||||
players = data.players || [];
|
||||
} else if (server.type === 'terraria') {
|
||||
// Use log parsing for Terraria (no RCON support)
|
||||
const data = await getTerrariaPlayers(server);
|
||||
players = data.players || [];
|
||||
}
|
||||
|
||||
const result = { players };
|
||||
|
||||
@@ -580,7 +580,7 @@ export async function writePalworldConfig(server, filename, content) {
|
||||
}
|
||||
|
||||
// ============ TERRARIA CONFIG ============
|
||||
const TERRARIA_CONFIG_PATH = "/home/terraria/serverconfig.txt";
|
||||
const TERRARIA_CONFIG_PATH = "/home/terraria/tModLoader/serverconfig.txt";
|
||||
|
||||
export async function readTerrariaConfig(server) {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
@@ -598,7 +598,7 @@ export async function writeTerrariaConfig(server, content) {
|
||||
|
||||
// Create backup
|
||||
const backupName = `serverconfig.txt.backup.${Date.now()}`;
|
||||
await ssh.execCommand(`cp ${TERRARIA_CONFIG_PATH} /home/terraria/${backupName} 2>/dev/null || true`);
|
||||
await ssh.execCommand(`cp ${TERRARIA_CONFIG_PATH} /home/terraria/tModLoader/${backupName} 2>/dev/null || true`);
|
||||
|
||||
// Write file using sftp
|
||||
const sftp = await ssh.requestSFTP();
|
||||
@@ -611,7 +611,7 @@ export async function writeTerrariaConfig(server, content) {
|
||||
});
|
||||
|
||||
// Clean up old backups (keep last 5)
|
||||
await ssh.execCommand(`ls -t /home/terraria/serverconfig.txt.backup.* 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true`);
|
||||
await ssh.execCommand(`ls -t /home/terraria/tModLoader/serverconfig.txt.backup.* 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true`);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -643,6 +643,57 @@ export async function writeOpenTTDConfig(server, content) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============ TERRARIA FUNCTIONS ============
|
||||
// Get Terraria players by parsing PM2 logs
|
||||
export async function getTerrariaPlayers(server) {
|
||||
try {
|
||||
const ssh = await getConnection(server.host, server.sshUser);
|
||||
|
||||
// Get last 500 lines of PM2 logs
|
||||
const nvmPrefix = "source ~/.nvm/nvm.sh && ";
|
||||
const result = await ssh.execCommand(nvmPrefix + `pm2 logs ${server.serviceName} --lines 500 --nostream 2>/dev/null | grep -E "ist beigetreten|hat das Spiel verlassen|has joined|has left" | tail -100`);
|
||||
|
||||
const players = new Map(); // PlayerName -> true
|
||||
|
||||
const lines = result.stdout.split('\n').filter(l => l.trim());
|
||||
for (const line of lines) {
|
||||
// German: "Lokführer ist beigetreten." / "Lokführer hat das Spiel verlassen."
|
||||
// English: "PlayerName has joined." / "PlayerName has left."
|
||||
|
||||
// Join patterns
|
||||
const joinMatchDE = line.match(/^\d+\|[^\|]+\s*\|\s*(.+?)\s+ist beigetreten\.?$/i);
|
||||
const joinMatchEN = line.match(/^\d+\|[^\|]+\s*\|\s*(.+?)\s+has joined\.?$/i);
|
||||
|
||||
if (joinMatchDE) {
|
||||
players.set(joinMatchDE[1].trim(), true);
|
||||
continue;
|
||||
}
|
||||
if (joinMatchEN) {
|
||||
players.set(joinMatchEN[1].trim(), true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Leave patterns
|
||||
const leaveMatchDE = line.match(/^\d+\|[^\|]+\s*\|\s*(.+?)\s+hat das Spiel verlassen\.?$/i);
|
||||
const leaveMatchEN = line.match(/^\d+\|[^\|]+\s*\|\s*(.+?)\s+has left\.?$/i);
|
||||
|
||||
if (leaveMatchDE) {
|
||||
players.delete(leaveMatchDE[1].trim());
|
||||
continue;
|
||||
}
|
||||
if (leaveMatchEN) {
|
||||
players.delete(leaveMatchEN[1].trim());
|
||||
}
|
||||
}
|
||||
|
||||
const playerList = Array.from(players.keys());
|
||||
return { online: playerList.length, players: playerList };
|
||||
} catch (err) {
|
||||
console.error(`[Terraria] Error getting players:`, err.message);
|
||||
return { online: 0, players: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// ============ HYTALE FUNCTIONS ============
|
||||
const HYTALE_CONFIG_PATH = "/opt/hytale/Server/config.json";
|
||||
const HYTALE_LOGS_PATH = "/opt/hytale/Server/logs";
|
||||
|
||||
Reference in New Issue
Block a user