105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
/**
|
|
* Syncs Minecraft server data (players, plugins) into the local database
|
|
* by parsing server logs and using RCON commands.
|
|
*/
|
|
|
|
import { db } from "@/lib/db";
|
|
import { mcPlayers, plugins } from "@/lib/db/schema";
|
|
import { rconClient } from "@/lib/minecraft/rcon";
|
|
import { eq } from "drizzle-orm";
|
|
import { nanoid } from "nanoid";
|
|
|
|
/** Parse the player list from the RCON "list" command response. */
|
|
export async function syncOnlinePlayers(): Promise<void> {
|
|
try {
|
|
const response = await rconClient.sendCommand("list");
|
|
// Response format: "There are X of a max of Y players online: player1, player2"
|
|
const match = response.match(/players online: (.*)$/);
|
|
if (!match) return;
|
|
|
|
const onlineNames = match[1]
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
|
|
// Mark all players as offline first
|
|
await db
|
|
.update(mcPlayers)
|
|
.set({ isOnline: false })
|
|
.where(eq(mcPlayers.isOnline, true));
|
|
|
|
// Mark online players
|
|
for (const name of onlineNames) {
|
|
const existing = await db
|
|
.select()
|
|
.from(mcPlayers)
|
|
.where(eq(mcPlayers.username, name))
|
|
.get();
|
|
|
|
if (existing) {
|
|
await db
|
|
.update(mcPlayers)
|
|
.set({ isOnline: true, lastSeen: Date.now() })
|
|
.where(eq(mcPlayers.username, name));
|
|
}
|
|
}
|
|
} catch {
|
|
// RCON might not be connected — ignore
|
|
}
|
|
}
|
|
|
|
/** Parse a log line and update player records accordingly. */
|
|
export function parseLogLine(
|
|
line: string,
|
|
onPlayerJoin?: (name: string) => void,
|
|
onPlayerLeave?: (name: string) => void,
|
|
): void {
|
|
// "[HH:MM:SS] [Server thread/INFO]: PlayerName joined the game"
|
|
const joinMatch = line.match(/\[.*\]: (\w+) joined the game/);
|
|
if (joinMatch) {
|
|
const name = joinMatch[1];
|
|
upsertPlayer(name, { isOnline: true, lastSeen: Date.now() });
|
|
onPlayerJoin?.(name);
|
|
return;
|
|
}
|
|
|
|
// "[HH:MM:SS] [Server thread/INFO]: PlayerName left the game"
|
|
const leaveMatch = line.match(/\[.*\]: (\w+) left the game/);
|
|
if (leaveMatch) {
|
|
const name = leaveMatch[1];
|
|
upsertPlayer(name, { isOnline: false, lastSeen: Date.now() });
|
|
onPlayerLeave?.(name);
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function upsertPlayer(
|
|
username: string,
|
|
data: Partial<typeof mcPlayers.$inferInsert>,
|
|
): Promise<void> {
|
|
const existing = await db
|
|
.select()
|
|
.from(mcPlayers)
|
|
.where(eq(mcPlayers.username, username))
|
|
.get();
|
|
|
|
if (existing) {
|
|
await db
|
|
.update(mcPlayers)
|
|
.set(data as Record<string, unknown>)
|
|
.where(eq(mcPlayers.username, username));
|
|
} else {
|
|
await db.insert(mcPlayers).values({
|
|
id: nanoid(),
|
|
uuid: (data as { uuid?: string }).uuid ?? nanoid(), // placeholder until real UUID is known
|
|
username,
|
|
firstSeen: Date.now(),
|
|
lastSeen: Date.now(),
|
|
isOnline: false,
|
|
playTime: 0,
|
|
isBanned: false,
|
|
...data,
|
|
});
|
|
}
|
|
}
|